This is an automated email from the ASF dual-hosted git repository.
slachiewicz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-dependency-analyzer.git
The following commit(s) were added to refs/heads/master by this push:
new 788fb61 Treat classless declared aggregator as used when its
dependency is used
788fb61 is described below
commit 788fb61e8315002c07eb4e439183a8161ae4f8c8
Author: Sylwester Lachiewicz <[email protected]>
AuthorDate: Thu Oct 29 20:51:19 2026 +0000
Treat classless declared aggregator as used when its dependency is used
A declared jar with no classes (junit-jupiter) was reported unused while
its API jar was reported used undeclared; declaring the API jar made the
aggregator pointless.
---
.../analyzer/DefaultProjectDependencyAnalyzer.java | 52 +++++++++++++++
.../DefaultProjectDependencyAnalyzerTest.java | 75 ++++++++++++++++++++++
2 files changed, 127 insertions(+)
diff --git
a/src/main/java/org/apache/maven/shared/dependency/analyzer/DefaultProjectDependencyAnalyzer.java
b/src/main/java/org/apache/maven/shared/dependency/analyzer/DefaultProjectDependencyAnalyzer.java
index d459809..e903b52 100644
---
a/src/main/java/org/apache/maven/shared/dependency/analyzer/DefaultProjectDependencyAnalyzer.java
+++
b/src/main/java/org/apache/maven/shared/dependency/analyzer/DefaultProjectDependencyAnalyzer.java
@@ -27,6 +27,7 @@ import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayDeque;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
@@ -142,6 +143,7 @@ public class DefaultProjectDependencyAnalyzer implements
ProjectDependencyAnalyz
Map<Artifact, Set<DependencyUsage>> usedArtifacts =
buildUsedArtifacts(classToArtifactMap, dependencyClasses);
+ creditClasslessAggregators(artifactClassMap, declaredArtifacts,
usedArtifacts);
Set<Artifact> mainUsedArtifacts =
buildUsedArtifacts(classToArtifactMap, mainDependencyClasses)
.keySet();
@@ -470,6 +472,56 @@ public class DefaultProjectDependencyAnalyzer implements
ProjectDependencyAnalyz
return usedArtifacts;
}
+ /**
+ * Treats a declared artifact without classes of its own (for example
{@code junit-jupiter}, which only carries
+ * {@code module-info.class}) as used when one of the artifacts it brings
in directly is used. The used artifacts
+ * that are brought in by such an aggregator and are not declared
themselves are then no longer reported as used
+ * undeclared. The direct dependencies are taken from the dependency trail
of the resolved artifacts, so only the
+ * path that survived dependency mediation is considered.
+ *
+ * @param artifactClassMap the classes of each resolved artifact
+ * @param declaredArtifacts the artifacts declared by the project
+ * @param usedArtifacts the used artifacts, updated in place
+ */
+ static void creditClasslessAggregators(
+ Map<Artifact, Set<String>> artifactClassMap,
+ Set<Artifact> declaredArtifacts,
+ Map<Artifact, Set<DependencyUsage>> usedArtifacts) {
+ Set<String> declaredIds = declaredArtifacts.stream()
+ .map(Artifact::getDependencyConflictId)
+ .collect(Collectors.toSet());
+
+ for (Map.Entry<Artifact, Set<String>> entry :
artifactClassMap.entrySet()) {
+ Artifact aggregator = entry.getKey();
+ if (!entry.getValue().isEmpty()
+ ||
!declaredIds.contains(aggregator.getDependencyConflictId())
+ || usedArtifacts.containsKey(aggregator)) {
+ continue;
+ }
+
+ String prefix = aggregator.getGroupId() + ":" +
aggregator.getArtifactId() + ":";
+ Set<DependencyUsage> usages = new HashSet<>();
+ List<Artifact> usedChildren = new ArrayList<>();
+ for (Artifact used : usedArtifacts.keySet()) {
+ List<String> trail = used.getDependencyTrail();
+ if (trail != null && trail.size() == 3 &&
trail.get(1).startsWith(prefix)) {
+ usedChildren.add(used);
+ usages.addAll(usedArtifacts.get(used));
+ }
+ }
+ if (usedChildren.isEmpty()) {
+ continue;
+ }
+
+ usedArtifacts.put(aggregator, usages);
+ for (Artifact child : usedChildren) {
+ if (!declaredIds.contains(child.getDependencyConflictId())) {
+ usedArtifacts.remove(child);
+ }
+ }
+ }
+ }
+
// MSHARED-47 an uncommon case where a commonly used
// third party dependency was added to the JDK
static boolean includedInJDK(Artifact artifact) {
diff --git
a/src/test/java/org/apache/maven/shared/dependency/analyzer/DefaultProjectDependencyAnalyzerTest.java
b/src/test/java/org/apache/maven/shared/dependency/analyzer/DefaultProjectDependencyAnalyzerTest.java
index b7ee223..d9c5f22 100644
---
a/src/test/java/org/apache/maven/shared/dependency/analyzer/DefaultProjectDependencyAnalyzerTest.java
+++
b/src/test/java/org/apache/maven/shared/dependency/analyzer/DefaultProjectDependencyAnalyzerTest.java
@@ -168,6 +168,81 @@ class DefaultProjectDependencyAnalyzerTest {
assertThat(used.keySet()).containsExactly(plexusXml);
}
+ @Test
+ void testClasslessDeclaredAggregatorIsUsedWhenItsDirectDependencyIsUsed() {
+ Artifact jupiter = aTestArtifact("junit-jupiter");
+ Artifact api = aTestArtifact("junit-jupiter-api");
+ api.setDependencyTrail(Arrays.asList(
+ "groupId:project:jar:1.0", "groupId:junit-jupiter:jar:1.0",
"groupId:junit-jupiter-api:jar:1.0"));
+
+ Map<Artifact, Set<String>> artifactClassMap = new LinkedHashMap<>();
+ artifactClassMap.put(jupiter, Collections.emptySet());
+ artifactClassMap.put(api,
Collections.singleton("org.junit.jupiter.api.Test"));
+
+ Map<Artifact, Set<DependencyUsage>> used = new LinkedHashMap<>();
+ used.put(api, Collections.singleton(new
DependencyUsage("org.junit.jupiter.api.Test", "Main")));
+
+ DefaultProjectDependencyAnalyzer.creditClasslessAggregators(
+ artifactClassMap, Collections.singleton(jupiter), used);
+
+ assertThat(used.keySet()).containsExactly(jupiter);
+ assertThat(used.get(jupiter)).hasSize(1);
+ }
+
+ @Test
+ void
testClasslessDeclaredAggregatorStaysUnusedWhenNothingItBringsInIsUsed() {
+ Artifact jupiter = aTestArtifact("junit-jupiter");
+
+ Map<Artifact, Set<String>> artifactClassMap = new LinkedHashMap<>();
+ artifactClassMap.put(jupiter, Collections.emptySet());
+
+ Map<Artifact, Set<DependencyUsage>> used = new LinkedHashMap<>();
+
+ DefaultProjectDependencyAnalyzer.creditClasslessAggregators(
+ artifactClassMap, Collections.singleton(jupiter), used);
+
+ assertThat(used).isEmpty();
+ }
+
+ @Test
+ void testDeclaredChildOfClasslessAggregatorKeepsItsOwnCredit() {
+ Artifact jupiter = aTestArtifact("junit-jupiter");
+ Artifact api = aTestArtifact("junit-jupiter-api");
+ api.setDependencyTrail(Arrays.asList(
+ "groupId:project:jar:1.0", "groupId:junit-jupiter:jar:1.0",
"groupId:junit-jupiter-api:jar:1.0"));
+
+ Map<Artifact, Set<String>> artifactClassMap = new LinkedHashMap<>();
+ artifactClassMap.put(jupiter, Collections.emptySet());
+ artifactClassMap.put(api,
Collections.singleton("org.junit.jupiter.api.Test"));
+
+ Map<Artifact, Set<DependencyUsage>> used = new LinkedHashMap<>();
+ used.put(api, Collections.singleton(new
DependencyUsage("org.junit.jupiter.api.Test", "Main")));
+
+ DefaultProjectDependencyAnalyzer.creditClasslessAggregators(
+ artifactClassMap, new HashSet<>(Arrays.asList(jupiter, api)),
used);
+
+ assertThat(used.keySet()).containsExactlyInAnyOrder(jupiter, api);
+ }
+
+ @Test
+ void testAggregatorWithClassesIsNotTouched() {
+ Artifact lib = aTestArtifact("lib");
+ Artifact child = aTestArtifact("child");
+ child.setDependencyTrail(
+ Arrays.asList("groupId:project:jar:1.0",
"groupId:lib:jar:1.0", "groupId:child:jar:1.0"));
+
+ Map<Artifact, Set<String>> artifactClassMap = new LinkedHashMap<>();
+ artifactClassMap.put(lib, Collections.singleton("lib.Foo"));
+ artifactClassMap.put(child, Collections.singleton("child.Bar"));
+
+ Map<Artifact, Set<DependencyUsage>> used = new LinkedHashMap<>();
+ used.put(child, Collections.singleton(new DependencyUsage("child.Bar",
"Main")));
+
+
DefaultProjectDependencyAnalyzer.creditClasslessAggregators(artifactClassMap,
Collections.singleton(lib), used);
+
+ assertThat(used.keySet()).containsExactly(child);
+ }
+
@Test
void testBuildClassToArtifactMapWithMultipleClasses() {
Artifact artifact1 = aTestArtifact("artifact1");