This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven.git
The following commit(s) were added to refs/heads/master by this push:
new ea308a5e7a fix: preserve processor path types across conflict
resolution (#11805)
ea308a5e7a is described below
commit ea308a5e7a6e91d814769cd5bc639eee1a907177
Author: Mariusz <[email protected]>
AuthorDate: Sat Mar 28 12:22:05 2026 +0100
fix: preserve processor path types across conflict resolution (#11805)
When a dependency is both a direct dep (e.g., modular-jar) and a
transitive dep of a processor, ConflictResolver removes the transitive
occurrence before TypeDeriver can assign processor path types.
This causes the artifact to appear only on --module-path, not on
--processor-module-path (maven-compiler-plugin#1039).
Fix: two-phase approach without changing maven-resolver:
- TypeCollector runs BEFORE ConflictResolver, records which artifacts
are transitive deps of processors in the transformation context
- TypeDeriver (after ConflictResolver) reads the context and sets
PROCESSOR_TYPE property on surviving nodes that need both paths
- DefaultDependencyResolverResult reads PROCESSOR_TYPE and adds the
artifact to processor paths as well
TypeCollector short-circuits when no processor-type direct deps exist,
so the vast majority of builds pay zero cost.
---
.../internal/MavenSessionBuilderSupplier.java | 2 +
.../internal/artifact/MavenArtifactProperties.java | 8 +
.../repository/internal/type/TypeCollector.java | 92 ++++++++
.../repository/internal/type/TypeDeriver.java | 40 +++-
.../impl/DefaultDependencyResolverResult.java | 48 ++++
.../impl/resolver/MavenSessionBuilderSupplier.java | 2 +
.../resolver/artifact/MavenArtifactProperties.java | 10 +
.../maven/impl/resolver/type/TypeCollector.java | 112 ++++++++++
.../maven/impl/resolver/type/TypeDeriver.java | 52 ++++-
.../TypeDeriverWithConflictResolutionTest.java | 247 +++++++++++++++++++++
10 files changed, 611 insertions(+), 2 deletions(-)
diff --git
a/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/MavenSessionBuilderSupplier.java
b/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/MavenSessionBuilderSupplier.java
index 5d1159f09e..8558bddc50 100644
---
a/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/MavenSessionBuilderSupplier.java
+++
b/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/MavenSessionBuilderSupplier.java
@@ -25,6 +25,7 @@
import org.apache.maven.repository.internal.artifact.FatArtifactTraverser;
import
org.apache.maven.repository.internal.scopes.Maven4ScopeManagerConfiguration;
import org.apache.maven.repository.internal.type.DefaultTypeProvider;
+import org.apache.maven.repository.internal.type.TypeCollector;
import org.apache.maven.repository.internal.type.TypeDeriver;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession.CloseableSession;
@@ -112,6 +113,7 @@ protected DependencySelector getDependencySelector() {
protected DependencyGraphTransformer getDependencyGraphTransformer() {
return new ChainedDependencyGraphTransformer(
+ new TypeCollector(),
new ConflictResolver(
new ConfigurableVersionSelector(), new
ManagedScopeSelector(getScopeManager()),
new SimpleOptionalitySelector(), new
ManagedScopeDeriver(getScopeManager())),
diff --git
a/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/artifact/MavenArtifactProperties.java
b/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/artifact/MavenArtifactProperties.java
index 1c3d93b613..64afdffcd9 100644
---
a/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/artifact/MavenArtifactProperties.java
+++
b/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/artifact/MavenArtifactProperties.java
@@ -44,6 +44,14 @@ public final class MavenArtifactProperties {
*/
public static final String CONSTITUTES_BUILD_PATH = "constitutesBuildPath";
+ /**
+ * When an artifact is both a regular dependency and a transitive
dependency
+ * of a processor, this property records the derived processor type ID.
+ *
+ * @since 4.0.0
+ */
+ public static final String PROCESSOR_TYPE = "maven.processor.type";
+
/**
* The (expected) path to the artifact on the local filesystem. An
artifact which has this property set is assumed
* to be not present in any regular repository and likewise has no
artifact descriptor. Artifact resolution will
diff --git
a/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/type/TypeCollector.java
b/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/type/TypeCollector.java
new file mode 100644
index 0000000000..4b127a1aa1
--- /dev/null
+++
b/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/type/TypeCollector.java
@@ -0,0 +1,92 @@
+/*
+ * 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.repository.internal.type;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.maven.api.Type;
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.artifact.ArtifactProperties;
+import org.eclipse.aether.collection.DependencyGraphTransformationContext;
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * Collects processor type information from the dependency graph BEFORE
conflict resolution.
+ *
+ * @since 4.0.0
+ * @deprecated since 4.0.0, use {@code maven-api-impl} jar instead
+ * @see TypeDeriver
+ */
+@Deprecated(since = "4.0.0")
+public class TypeCollector implements DependencyGraphTransformer {
+
+ public static final Object CONTEXT_KEY = TypeCollector.class.getName() +
".processorTypes";
+
+ static final Set<String> PROCESSOR_TYPE_IDS =
+ Set.of(Type.PROCESSOR, Type.CLASSPATH_PROCESSOR,
Type.MODULAR_PROCESSOR);
+
+ private static final Map<String, String> DERIVE_MAP = Map.of(
+ Type.JAR, Type.PROCESSOR,
+ Type.CLASSPATH_JAR, Type.CLASSPATH_PROCESSOR,
+ Type.MODULAR_JAR, Type.MODULAR_PROCESSOR);
+
+ @Override
+ public DependencyNode transformGraph(DependencyNode root,
DependencyGraphTransformationContext context)
+ throws RepositoryException {
+ Map<String, String> processorTypes = null;
+ for (DependencyNode child : root.getChildren()) {
+ if (child.getArtifact() == null) {
+ continue;
+ }
+ String childType =
child.getArtifact().getProperty(ArtifactProperties.TYPE, "");
+ if (!PROCESSOR_TYPE_IDS.contains(childType)) {
+ continue;
+ }
+ for (DependencyNode transitive : child.getChildren()) {
+ if (transitive.getArtifact() == null) {
+ continue;
+ }
+ String transitiveType =
transitive.getArtifact().getProperty(ArtifactProperties.TYPE, "");
+ String derived = DERIVE_MAP.get(transitiveType);
+ if (derived != null) {
+ if (processorTypes == null) {
+ processorTypes = new HashMap<>();
+ }
+ processorTypes.put(conflictKey(transitive), derived);
+ }
+ }
+ }
+ if (processorTypes != null) {
+ context.put(CONTEXT_KEY, processorTypes);
+ }
+ return root;
+ }
+
+ /**
+ * Builds a unique key for an artifact based on the same identity
components
+ * used by conflict resolution: groupId, artifactId, extension, and
classifier.
+ */
+ static String conflictKey(DependencyNode node) {
+ var a = node.getArtifact();
+ return a.getGroupId() + ':' + a.getArtifactId() + ':' +
a.getExtension() + ':' + a.getClassifier();
+ }
+}
diff --git
a/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/type/TypeDeriver.java
b/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/type/TypeDeriver.java
index 2120b4a587..fc7b5213f7 100644
---
a/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/type/TypeDeriver.java
+++
b/compat/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/type/TypeDeriver.java
@@ -25,6 +25,7 @@
import java.util.Set;
import org.apache.maven.api.Type;
+import org.apache.maven.repository.internal.artifact.MavenArtifactProperties;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.ArtifactProperties;
import org.eclipse.aether.artifact.ArtifactType;
@@ -52,6 +53,7 @@ public class TypeDeriver implements
DependencyGraphTransformer {
@Override
public DependencyNode transformGraph(DependencyNode root,
DependencyGraphTransformationContext context) {
+ ArtifactTypeRegistry registry =
context.getSession().getArtifactTypeRegistry();
if (logger.isDebugEnabled()) {
StringBuilder sb = new StringBuilder();
root.accept(new DependencyGraphDumper(
@@ -60,7 +62,12 @@ public DependencyNode transformGraph(DependencyNode root,
DependencyGraphTransfo
List.of(DependencyGraphDumper.artifactProperties(List.of(ArtifactProperties.TYPE))))));
logger.debug("TYPES: Before transform:\n {}", sb);
}
- root.accept(new
TypeDeriverVisitor(context.getSession().getArtifactTypeRegistry()));
+ root.accept(new TypeDeriverVisitor(registry));
+ @SuppressWarnings("unchecked")
+ Map<String, String> collectedProcessorTypes = (Map<String, String>)
context.get(TypeCollector.CONTEXT_KEY);
+ if (collectedProcessorTypes != null) {
+ root.accept(new ProcessorTypeMerger(collectedProcessorTypes));
+ }
if (logger.isDebugEnabled()) {
StringBuilder sb = new StringBuilder();
root.accept(new DependencyGraphDumper(
@@ -144,4 +151,35 @@ private ArtifactType derive(ArtifactType parentType,
ArtifactType currentType) {
return result;
}
}
+
+ private static class ProcessorTypeMerger implements DependencyVisitor {
+ private final Map<String, String> collectedProcessorTypes;
+
+ ProcessorTypeMerger(Map<String, String> collectedProcessorTypes) {
+ this.collectedProcessorTypes = collectedProcessorTypes;
+ }
+
+ @Override
+ public boolean visitEnter(DependencyNode node) {
+ if (node.getArtifact() != null) {
+ String currentType =
node.getArtifact().getProperty(ArtifactProperties.TYPE, "");
+ if (!TypeCollector.PROCESSOR_TYPE_IDS.contains(currentType)) {
+ String key = TypeCollector.conflictKey(node);
+ String processorType = collectedProcessorTypes.get(key);
+ if (processorType != null) {
+ Artifact artifact = node.getArtifact();
+ Map<String, String> props = new
HashMap<>(artifact.getProperties());
+ props.put(MavenArtifactProperties.PROCESSOR_TYPE,
processorType);
+ node.setArtifact(artifact.setProperties(props));
+ }
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean visitLeave(DependencyNode node) {
+ return true;
+ }
+ }
}
diff --git
a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultDependencyResolverResult.java
b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultDependencyResolverResult.java
index a97062ae5a..89ffd472bf 100644
---
a/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultDependencyResolverResult.java
+++
b/impl/maven-impl/src/main/java/org/apache/maven/impl/DefaultDependencyResolverResult.java
@@ -36,9 +36,11 @@
import org.apache.maven.api.JavaPathType;
import org.apache.maven.api.Node;
import org.apache.maven.api.PathType;
+import org.apache.maven.api.Type;
import org.apache.maven.api.services.DependencyResolverException;
import org.apache.maven.api.services.DependencyResolverRequest;
import org.apache.maven.api.services.DependencyResolverResult;
+import org.apache.maven.impl.resolver.artifact.MavenArtifactProperties;
/**
* The result of collecting dependencies with a dependency resolver.
@@ -320,6 +322,52 @@ void addDependency(Node node, Dependency dep,
Predicate<PathType> filter, Path p
}
}
addPathElement(cache.selectPathType(pathTypes, filter,
path).orElse(PathType.UNRESOLVED), path);
+ // If the artifact is also needed on a processor path (because it's a
transitive dep
+ // of a processor AND a direct dep with a different type), add it to
the processor path too.
+ addProcessorPathIfNeeded(node, filter, path);
+ }
+
+ /**
+ * Checks if the artifact has a {@link
MavenArtifactProperties#PROCESSOR_TYPE} property
+ * and, if so, also adds it to the corresponding processor path. This
handles the case
+ * where an artifact is both a regular dependency (e.g., modular-jar on
--module-path)
+ * and a transitive dependency of a processor (needs
--processor-module-path).
+ */
+ private void addProcessorPathIfNeeded(Node node, Predicate<PathType>
filter, Path path) throws IOException {
+ if (!(node instanceof AbstractNode abstractNode)) {
+ return;
+ }
+ org.eclipse.aether.artifact.Artifact aetherArtifact =
+ abstractNode.getDependencyNode().getArtifact();
+ if (aetherArtifact == null) {
+ return;
+ }
+ String processorType =
aetherArtifact.getProperty(MavenArtifactProperties.PROCESSOR_TYPE, null);
+ if (processorType == null) {
+ return;
+ }
+ Set<PathType> processorPathTypes =
processorPathTypesFor(processorType);
+ if (processorPathTypes != null) {
+ cache.selectPathType(processorPathTypes, filter,
path).ifPresent(pt -> addPathElement(pt, path));
+ }
+ }
+
+ // Path type sets for processor types — must stay in sync with
DefaultTypeProvider
+ private static final Set<PathType> PROCESSOR_PATH_TYPES =
+ Set.of(JavaPathType.PROCESSOR_CLASSES,
JavaPathType.PROCESSOR_MODULES);
+ private static final Set<PathType> CLASSPATH_PROCESSOR_PATH_TYPES =
Set.of(JavaPathType.PROCESSOR_CLASSES);
+ private static final Set<PathType> MODULAR_PROCESSOR_PATH_TYPES =
Set.of(JavaPathType.PROCESSOR_MODULES);
+
+ /**
+ * Maps a processor type ID to its corresponding path types.
+ */
+ private static Set<PathType> processorPathTypesFor(String processorType) {
+ return switch (processorType) {
+ case Type.PROCESSOR -> PROCESSOR_PATH_TYPES;
+ case Type.CLASSPATH_PROCESSOR -> CLASSPATH_PROCESSOR_PATH_TYPES;
+ case Type.MODULAR_PROCESSOR -> MODULAR_PROCESSOR_PATH_TYPES;
+ default -> null;
+ };
}
/**
diff --git
a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/MavenSessionBuilderSupplier.java
b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/MavenSessionBuilderSupplier.java
index f7b4c237dd..b6a0a711e8 100644
---
a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/MavenSessionBuilderSupplier.java
+++
b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/MavenSessionBuilderSupplier.java
@@ -26,6 +26,7 @@
import org.apache.maven.impl.resolver.scopes.Maven3ScopeManagerConfiguration;
import org.apache.maven.impl.resolver.scopes.Maven4ScopeManagerConfiguration;
import org.apache.maven.impl.resolver.type.DefaultTypeProvider;
+import org.apache.maven.impl.resolver.type.TypeCollector;
import org.apache.maven.impl.resolver.type.TypeDeriver;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession.CloseableSession;
@@ -108,6 +109,7 @@ protected DependencySelector getDependencySelector() {
protected DependencyGraphTransformer getDependencyGraphTransformer() {
return new ChainedDependencyGraphTransformer(
+ new TypeCollector(),
new ConflictResolver(
new ConfigurableVersionSelector(), new
ManagedScopeSelector(getScopeManager()),
new SimpleOptionalitySelector(), new
ManagedScopeDeriver(getScopeManager())),
diff --git
a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/artifact/MavenArtifactProperties.java
b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/artifact/MavenArtifactProperties.java
index 1ebfc84b80..d2aad0d7e9 100644
---
a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/artifact/MavenArtifactProperties.java
+++
b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/artifact/MavenArtifactProperties.java
@@ -42,6 +42,16 @@ public final class MavenArtifactProperties {
*/
public static final String CONSTITUTES_BUILD_PATH = "constitutesBuildPath";
+ /**
+ * When an artifact is both a regular dependency (e.g., modular-jar) and a
transitive dependency
+ * of a processor, this property records the derived processor type ID
(e.g., "modular-processor").
+ * This allows the artifact to be placed on both the module-path and the
processor-module-path.
+ *
+ * @since 4.0.0
+ * @see org.apache.maven.impl.resolver.type.TypeCollector
+ */
+ public static final String PROCESSOR_TYPE = "maven.processor.type";
+
/**
* The (expected) path to the artifact on the local filesystem. An
artifact which has this property set is assumed
* to be not present in any regular repository and likewise has no
artifact descriptor. Artifact resolution will
diff --git
a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/type/TypeCollector.java
b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/type/TypeCollector.java
new file mode 100644
index 0000000000..cde83689b9
--- /dev/null
+++
b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/type/TypeCollector.java
@@ -0,0 +1,112 @@
+/*
+ * 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.type;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.maven.api.Type;
+import org.eclipse.aether.RepositoryException;
+import org.eclipse.aether.artifact.ArtifactProperties;
+import org.eclipse.aether.collection.DependencyGraphTransformationContext;
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.graph.DependencyNode;
+
+/**
+ * Collects processor type information from the dependency graph BEFORE
conflict resolution.
+ * <p>
+ * For each direct dependency that is a processor type, this transformer
records which of
+ * its children (transitive deps) would need processor path types. This
information is stored
+ * in the transformation context so that {@link TypeDeriver} (which runs after
conflict resolution)
+ * can apply processor path types even to nodes whose transitive processor
occurrence
+ * was eliminated by conflict resolution.
+ * <p>
+ * Without this collector, the following scenario fails:
+ * <pre>
+ * root
+ * ├── shared-lib:1.0 (type=modular-jar) → --module-path
+ * └── my-processor:1.0 (type=modular-processor)
+ * └── shared-lib:1.0 (type=jar) → should go to
--processor-module-path
+ * </pre>
+ * ConflictResolver removes the transitive shared-lib (same GA, loser), so
TypeDeriver
+ * never sees it under the processor. This collector preserves that
information.
+ *
+ * @since 4.0.0
+ * @see TypeDeriver
+ */
+public class TypeCollector implements DependencyGraphTransformer {
+
+ /**
+ * Context key under which the collected processor type map is stored.
+ * The value is a {@code Map<String, String>} mapping artifact conflict
keys
+ * (groupId:artifactId:extension:classifier) to derived processor type IDs.
+ */
+ public static final Object CONTEXT_KEY = TypeCollector.class.getName() +
".processorTypes";
+
+ static final Set<String> PROCESSOR_TYPE_IDS =
+ Set.of(Type.PROCESSOR, Type.CLASSPATH_PROCESSOR,
Type.MODULAR_PROCESSOR);
+
+ private static final Map<String, String> DERIVE_MAP = Map.of(
+ Type.JAR, Type.PROCESSOR,
+ Type.CLASSPATH_JAR, Type.CLASSPATH_PROCESSOR,
+ Type.MODULAR_JAR, Type.MODULAR_PROCESSOR);
+
+ @Override
+ public DependencyNode transformGraph(DependencyNode root,
DependencyGraphTransformationContext context)
+ throws RepositoryException {
+ Map<String, String> processorTypes = null;
+ for (DependencyNode child : root.getChildren()) {
+ if (child.getArtifact() == null) {
+ continue;
+ }
+ String childType =
child.getArtifact().getProperty(ArtifactProperties.TYPE, "");
+ if (!PROCESSOR_TYPE_IDS.contains(childType)) {
+ continue;
+ }
+ // This direct dep is a processor — record its children's derived
types
+ for (DependencyNode transitive : child.getChildren()) {
+ if (transitive.getArtifact() == null) {
+ continue;
+ }
+ String transitiveType =
transitive.getArtifact().getProperty(ArtifactProperties.TYPE, "");
+ String derived = DERIVE_MAP.get(transitiveType);
+ if (derived != null) {
+ if (processorTypes == null) {
+ processorTypes = new HashMap<>();
+ }
+ processorTypes.put(conflictKey(transitive), derived);
+ }
+ }
+ }
+ if (processorTypes != null) {
+ context.put(CONTEXT_KEY, processorTypes);
+ }
+ return root;
+ }
+
+ /**
+ * Builds a unique key for an artifact based on the same identity
components
+ * used by conflict resolution: groupId, artifactId, extension, and
classifier.
+ */
+ static String conflictKey(DependencyNode node) {
+ var a = node.getArtifact();
+ return a.getGroupId() + ':' + a.getArtifactId() + ':' +
a.getExtension() + ':' + a.getClassifier();
+ }
+}
diff --git
a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/type/TypeDeriver.java
b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/type/TypeDeriver.java
index 2a83ed83b1..6343cd2cbf 100644
---
a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/type/TypeDeriver.java
+++
b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/type/TypeDeriver.java
@@ -25,6 +25,7 @@
import java.util.Set;
import org.apache.maven.api.Type;
+import org.apache.maven.impl.resolver.artifact.MavenArtifactProperties;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.ArtifactProperties;
import org.eclipse.aether.artifact.ArtifactType;
@@ -58,6 +59,7 @@ public class TypeDeriver implements
DependencyGraphTransformer {
@Override
public DependencyNode transformGraph(DependencyNode root,
DependencyGraphTransformationContext context) {
+ ArtifactTypeRegistry registry =
context.getSession().getArtifactTypeRegistry();
if (logger.isDebugEnabled()) {
StringBuilder sb = new StringBuilder();
root.accept(new DependencyGraphDumper(
@@ -66,7 +68,16 @@ public DependencyNode transformGraph(DependencyNode root,
DependencyGraphTransfo
List.of(DependencyGraphDumper.artifactProperties(List.of(ArtifactProperties.TYPE))))));
logger.debug("TYPES: Before transform:\n {}", sb);
}
- root.accept(new
TypeDeriverVisitor(context.getSession().getArtifactTypeRegistry()));
+ root.accept(new TypeDeriverVisitor(registry));
+ // Apply processor type info collected by TypeCollector before
conflict resolution.
+ // This handles the case where an artifact is both a direct dep (e.g.,
modular-jar)
+ // and a transitive dep of a processor — conflict resolution removes
the transitive
+ // occurrence, but TypeCollector preserved the processor type
information.
+ @SuppressWarnings("unchecked")
+ Map<String, String> collectedProcessorTypes = (Map<String, String>)
context.get(TypeCollector.CONTEXT_KEY);
+ if (collectedProcessorTypes != null) {
+ root.accept(new ProcessorTypeMerger(collectedProcessorTypes));
+ }
if (logger.isDebugEnabled()) {
StringBuilder sb = new StringBuilder();
root.accept(new DependencyGraphDumper(
@@ -150,4 +161,43 @@ private ArtifactType derive(ArtifactType parentType,
ArtifactType currentType) {
return result;
}
}
+
+ /**
+ * Visitor that merges processor type info from {@link TypeCollector} into
surviving nodes.
+ * For nodes that already have a processor type (handled by
TypeDeriverVisitor), this is a no-op.
+ * For nodes that lost their processor occurrence to conflict resolution,
this sets the
+ * {@link MavenArtifactProperties#PROCESSOR_TYPE} property so the artifact
gets added
+ * to processor paths as well.
+ */
+ private static class ProcessorTypeMerger implements DependencyVisitor {
+ private final Map<String, String> collectedProcessorTypes;
+
+ ProcessorTypeMerger(Map<String, String> collectedProcessorTypes) {
+ this.collectedProcessorTypes = collectedProcessorTypes;
+ }
+
+ @Override
+ public boolean visitEnter(DependencyNode node) {
+ if (node.getArtifact() != null) {
+ String currentType =
node.getArtifact().getProperty(ArtifactProperties.TYPE, "");
+ // Skip nodes that are already processor types (handled by
TypeDeriverVisitor)
+ if (!TypeCollector.PROCESSOR_TYPE_IDS.contains(currentType)) {
+ String key = TypeCollector.conflictKey(node);
+ String processorType = collectedProcessorTypes.get(key);
+ if (processorType != null) {
+ Artifact artifact = node.getArtifact();
+ Map<String, String> props = new
HashMap<>(artifact.getProperties());
+ props.put(MavenArtifactProperties.PROCESSOR_TYPE,
processorType);
+ node.setArtifact(artifact.setProperties(props));
+ }
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean visitLeave(DependencyNode node) {
+ return true;
+ }
+ }
}
diff --git
a/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/type/TypeDeriverWithConflictResolutionTest.java
b/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/type/TypeDeriverWithConflictResolutionTest.java
new file mode 100644
index 0000000000..45d873a261
--- /dev/null
+++
b/impl/maven-impl/src/test/java/org/apache/maven/impl/resolver/type/TypeDeriverWithConflictResolutionTest.java
@@ -0,0 +1,247 @@
+/*
+ * 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.type;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.apache.maven.api.Type;
+import org.apache.maven.api.services.TypeRegistry;
+import org.apache.maven.impl.resolver.artifact.MavenArtifactProperties;
+import org.apache.maven.impl.resolver.scopes.Maven4ScopeManagerConfiguration;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.ArtifactProperties;
+import org.eclipse.aether.artifact.ArtifactType;
+import org.eclipse.aether.artifact.ArtifactTypeRegistry;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.collection.DependencyGraphTransformer;
+import org.eclipse.aether.graph.DefaultDependencyNode;
+import org.eclipse.aether.graph.Dependency;
+import org.eclipse.aether.graph.DependencyNode;
+import
org.eclipse.aether.internal.impl.collect.DefaultDependencyGraphTransformationContext;
+import org.eclipse.aether.internal.impl.scope.ManagedScopeDeriver;
+import org.eclipse.aether.internal.impl.scope.ManagedScopeSelector;
+import org.eclipse.aether.internal.impl.scope.ScopeManagerImpl;
+import
org.eclipse.aether.util.graph.transformer.ChainedDependencyGraphTransformer;
+import org.eclipse.aether.util.graph.transformer.ConfigurableVersionSelector;
+import org.eclipse.aether.util.graph.transformer.ConflictResolver;
+import org.eclipse.aether.util.graph.transformer.SimpleOptionalitySelector;
+import org.eclipse.aether.version.Version;
+import org.eclipse.aether.version.VersionConstraint;
+import org.eclipse.aether.version.VersionRange;
+import org.junit.jupiter.api.Test;
+
+import static java.util.Objects.requireNonNull;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Demonstrates the conflict between ConflictResolver and TypeDeriver:
+ * when the same artifact is both a direct dependency (modular-jar)
+ * and a transitive dependency of a processor, ConflictResolver eliminates
+ * the transitive occurrence BEFORE TypeDeriver can assign processor path
types.
+ *
+ * Scenario (reproduces maven-compiler-plugin#1039):
+ * <pre>
+ * root (project)
+ * ├── shared-lib:1.0 (type=modular-jar) ← direct dependency, goes
to --module-path
+ * └── my-processor:1.0 (type=modular-processor) ← annotation processor
+ * └── shared-lib:1.0 (type=jar) ← transitive, SHOULD go
to --processor-module-path
+ * </pre>
+ *
+ * After conflict resolution, only one shared-lib node survives (the direct
one).
+ * TypeDeriver never sees the transitive occurrence, so it can't add processor
path types.
+ * Result: shared-lib ends up ONLY on --module-path, NOT on
--processor-module-path.
+ */
+class TypeDeriverWithConflictResolutionTest {
+ private final ArtifactTypeRegistry typeRegistry = new
TypeRegistryAdapter(new TypeRegistry() {
+ private final Map<String, Type> types =
+ new
DefaultTypeProvider().types().stream().collect(Collectors.toMap(DefaultType::id,
t -> t));
+
+ @Override
+ public Optional<Type> lookup(String id) {
+ return Optional.ofNullable(types.get(id));
+ }
+ });
+
+ /**
+ * This test demonstrates the problem: when a dependency is both a direct
dep (modular-jar)
+ * and a transitive dep of a processor, the full transformer chain
(ConflictResolver + TypeDeriver)
+ * loses the processor type information.
+ *
+ * The shared-lib should have BOTH modular-jar AND modular-processor path
type properties,
+ * but after conflict resolution it only retains modular-jar.
+ */
+ @Test
+ void sharedDependencyLosesProcessorType() throws Exception {
+ var scopeManager = new
ScopeManagerImpl(Maven4ScopeManagerConfiguration.INSTANCE);
+
+ RepositorySystemSession session = mock(RepositorySystemSession.class);
+ when(session.getArtifactTypeRegistry()).thenReturn(typeRegistry);
+ when(session.getConfigProperties()).thenReturn(Collections.emptyMap());
+
+ ArtifactType jar = requireNonNull(typeRegistry.get(Type.JAR));
+ ArtifactType modularJar =
requireNonNull(typeRegistry.get(Type.MODULAR_JAR));
+ ArtifactType modularProcessor =
requireNonNull(typeRegistry.get(Type.MODULAR_PROCESSOR));
+
+ // root: "the project"
+ DefaultDependencyNode root = new DefaultDependencyNode(new
DefaultArtifact("project:project:1.0", jar));
+
+ // direct dep: shared-lib as modular-jar (goes to --module-path)
+ DefaultDependencyNode directSharedLib =
depNode("com.example:shared-lib:1.0", modularJar, "compile");
+
+ // direct dep: annotation processor as modular-processor
+ DefaultDependencyNode processorNode =
depNode("com.example:my-processor:1.0", modularProcessor, "compile");
+
+ // transitive dep of processor: shared-lib as plain jar
+ // (this is how it appears in my-processor's POM — just a regular jar
dependency)
+ DefaultDependencyNode transitiveSharedLib =
depNode("com.example:shared-lib:1.0", jar, "compile");
+ processorNode.setChildren(new
ArrayList<>(List.of(transitiveSharedLib)));
+
+ root.setChildren(new ArrayList<>(List.of(directSharedLib,
processorNode)));
+
+ // Run the full transformer chain as configured in
MavenSessionBuilderSupplier:
+ // TypeCollector (before conflict resolution) → ConflictResolver →
TypeDeriver (after)
+ DependencyGraphTransformer transformer = new
ChainedDependencyGraphTransformer(
+ new TypeCollector(),
+ new ConflictResolver(
+ new ConfigurableVersionSelector(),
+ new ManagedScopeSelector(scopeManager),
+ new SimpleOptionalitySelector(),
+ new ManagedScopeDeriver(scopeManager)),
+ new TypeDeriver());
+
+ DependencyNode transformed =
+ transformer.transformGraph(root, new
DefaultDependencyGraphTransformationContext(session));
+
+ // Find the surviving shared-lib node
+ DependencyNode survivingSharedLib = findNode(transformed,
"com.example", "shared-lib");
+ assertNotNull(survivingSharedLib, "shared-lib should survive conflict
resolution");
+
+ // The main type should still be modular-jar (from the winning direct
dep)
+ String actualType =
survivingSharedLib.getArtifact().getProperty(ArtifactProperties.TYPE, "");
+ assertEquals(Type.MODULAR_JAR, actualType, "main type should remain
modular-jar");
+
+ // ASSERT: The PROCESSOR_TYPE property should be set, indicating the
artifact
+ // is also needed on --processor-module-path
+ String processorType =
+
survivingSharedLib.getArtifact().getProperty(MavenArtifactProperties.PROCESSOR_TYPE,
null);
+ assertEquals(
+ Type.PROCESSOR,
+ processorType,
+ "shared-lib should have PROCESSOR_TYPE property because it's a
transitive dep of a processor");
+ }
+
+ /**
+ * Control test: TypeDeriver alone (without ConflictResolver) correctly
derives
+ * processor types for transitive deps. This passes — proving TypeDeriver
logic is correct
+ * when conflict resolution doesn't interfere.
+ */
+ @Test
+ void typeDeriverAloneWorksCorrectly() throws Exception {
+ RepositorySystemSession session = mock(RepositorySystemSession.class);
+ when(session.getArtifactTypeRegistry()).thenReturn(typeRegistry);
+
+ ArtifactType jar = requireNonNull(typeRegistry.get(Type.JAR));
+ ArtifactType modularProcessor =
requireNonNull(typeRegistry.get(Type.MODULAR_PROCESSOR));
+
+ DefaultDependencyNode root = new DefaultDependencyNode(new
DefaultArtifact("project:project:1.0", jar));
+
+ // processor with a transitive jar dep
+ DefaultDependencyNode processorNode = new DefaultDependencyNode(
+ new Dependency(new
DefaultArtifact("com.example:my-processor:1.0", modularProcessor), "compile"));
+
+ DefaultDependencyNode transitiveLib = new DefaultDependencyNode(
+ new Dependency(new
DefaultArtifact("com.example:shared-lib:1.0", jar), "compile"));
+ processorNode.setChildren(new ArrayList<>(List.of(transitiveLib)));
+
+ root.setChildren(new ArrayList<>(List.of(processorNode)));
+
+ // Run ONLY TypeDeriver (no ConflictResolver)
+ TypeDeriver typeDeriver = new TypeDeriver();
+ typeDeriver.transformGraph(root, new
DefaultDependencyGraphTransformationContext(session));
+
+ // TypeDeriver correctly derives: jar under modularProcessor →
processor type
+ String derivedType =
transitiveLib.getArtifact().getProperty(ArtifactProperties.TYPE, "");
+ assertEquals(Type.PROCESSOR, derivedType, "TypeDeriver should derive
jar→processor under modularProcessor");
+ }
+
+ /**
+ * Creates a DefaultDependencyNode with a proper VersionConstraint
+ * (required by ConflictResolver's ConfigurableVersionSelector).
+ */
+ private static DefaultDependencyNode depNode(String coords, ArtifactType
type, String scope) {
+ DefaultDependencyNode node =
+ new DefaultDependencyNode(new Dependency(new
DefaultArtifact(coords, type), scope));
+ String version = node.getArtifact().getVersion();
+ node.setVersionConstraint(new SimpleVersionConstraint(new
SimpleVersion(version)));
+ node.setVersion(new SimpleVersion(version));
+ return node;
+ }
+
+ private static DependencyNode findNode(DependencyNode root, String
groupId, String artifactId) {
+ if (root.getArtifact() != null
+ && groupId.equals(root.getArtifact().getGroupId())
+ && artifactId.equals(root.getArtifact().getArtifactId())) {
+ return root;
+ }
+ for (DependencyNode child : root.getChildren()) {
+ DependencyNode found = findNode(child, groupId, artifactId);
+ if (found != null) {
+ return found;
+ }
+ }
+ return null;
+ }
+
+ private record SimpleVersion(String version) implements Version {
+ @Override
+ public int compareTo(Version o) {
+ return version.compareTo(o.toString());
+ }
+
+ @Override
+ public String toString() {
+ return version;
+ }
+ }
+
+ private record SimpleVersionConstraint(Version version) implements
VersionConstraint {
+ @Override
+ public VersionRange getRange() {
+ return null; // fixed version, no range
+ }
+
+ @Override
+ public Version getVersion() {
+ return version;
+ }
+
+ @Override
+ public boolean containsVersion(Version ver) {
+ return version.equals(ver);
+ }
+ }
+}