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 5d1159f09e43..8558bddc5025 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 1c3d93b61315..64afdffcd9f9 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 000000000000..4b127a1aa16b --- /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 PROCESSOR_TYPE_IDS = + Set.of(Type.PROCESSOR, Type.CLASSPATH_PROCESSOR, Type.MODULAR_PROCESSOR); + + private static final Map 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 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 2120b4a58727..3bc0b51da3ba 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; @@ -61,6 +62,11 @@ public DependencyNode transformGraph(DependencyNode root, DependencyGraphTransfo logger.debug("TYPES: Before transform:\n {}", sb); } root.accept(new TypeDeriverVisitor(context.getSession().getArtifactTypeRegistry())); + @SuppressWarnings("unchecked") + Map collectedProcessorTypes = (Map) 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 +150,35 @@ private ArtifactType derive(ArtifactType parentType, ArtifactType currentType) { return result; } } + + private static class ProcessorTypeMerger implements DependencyVisitor { + private final Map collectedProcessorTypes; + + ProcessorTypeMerger(Map 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 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 a97062ae5a3b..89ffd472bf8a 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 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 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 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 PROCESSOR_PATH_TYPES = + Set.of(JavaPathType.PROCESSOR_CLASSES, JavaPathType.PROCESSOR_MODULES); + private static final Set CLASSPATH_PROCESSOR_PATH_TYPES = Set.of(JavaPathType.PROCESSOR_CLASSES); + private static final Set MODULAR_PROCESSOR_PATH_TYPES = Set.of(JavaPathType.PROCESSOR_MODULES); + + /** + * Maps a processor type ID to its corresponding path types. + */ + private static Set 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 f7b4c237dd7f..b6a0a711e817 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 1ebfc84b8012..d2aad0d7e940 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 000000000000..cde83689b975 --- /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. + *

+ * 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. + *

+ * Without this collector, the following scenario fails: + *

+ *   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
+ * 
+ * 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} 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 PROCESSOR_TYPE_IDS = + Set.of(Type.PROCESSOR, Type.CLASSPATH_PROCESSOR, Type.MODULAR_PROCESSOR); + + private static final Map 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 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 2a83ed83b110..48b8ee7a1ae4 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; @@ -67,6 +68,15 @@ public DependencyNode transformGraph(DependencyNode root, DependencyGraphTransfo logger.debug("TYPES: Before transform:\n {}", sb); } root.accept(new TypeDeriverVisitor(context.getSession().getArtifactTypeRegistry())); + // 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 collectedProcessorTypes = (Map) 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 +160,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 collectedProcessorTypes; + + ProcessorTypeMerger(Map 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 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 000000000000..45d873a26185 --- /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): + *
+ *   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
+ * 
+ * + * 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 types = + new DefaultTypeProvider().types().stream().collect(Collectors.toMap(DefaultType::id, t -> t)); + + @Override + public Optional 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); + } + } +}