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);
+        }
+    }
+}


Reply via email to