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");

Reply via email to