Skip to content

Commit 08a1012

Browse files
authored
Add shading analyzer (#4)
Signed-off-by: Jakub Stejskal <xstejs24@gmail.com>
1 parent 55bbd1c commit 08a1012

File tree

7 files changed

+876
-2
lines changed

7 files changed

+876
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ out/
1313
/bootstrap
1414
/dependencies.xml
1515
.java-version
16+
logs

checkstyle/checkstyle-suppressions.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,12 @@ under the License.
3131
<!-- Allow magic numbers in Maven plugin parameter defaults -->
3232
<suppress checks="MagicNumber" files=".*Mojo\.java"/>
3333

34-
<!-- Allow hidden fields for Maven plugin parameters -->
34+
<!-- Allow hidden fields for Maven plugin parameters and constructor parameters -->
3535
<suppress checks="HiddenField" files=".*Mojo\.java"/>
36+
<suppress checks="HiddenField" files=".*\.java"/>
37+
38+
<!-- Allow single-line if statements without braces -->
39+
<suppress checks="NeedBraces" files=".*\.java"/>
3640

3741
<!-- Allow protected/public fields for Maven plugin parameters -->
3842
<suppress checks="VisibilityModifier" files=".*Mojo\.java"/>

src/main/java/com/github/streamshub/AbstractAlignmentReporterMojo.java

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,42 @@ public abstract class AbstractAlignmentReporterMojo extends AbstractMojo {
146146
@Parameter(property = "excludes")
147147
private String excludes;
148148

149+
/**
150+
* A comma-separated list of module names to exclude from processing in multi-module projects, or <code>null</code> to
151+
* include all modules. Each module name pattern supports full and partial <code>*</code> wildcards.
152+
* <p>
153+
* For example, <code>*-test</code> will exclude all modules whose name ends with "-test",
154+
* and <code>flink-*</code> will exclude all modules whose name starts with "flink-".
155+
* </p>
156+
*/
157+
@Parameter(property = "excludeModules")
158+
private String excludeModules;
159+
160+
/**
161+
* Enable shade-aware analysis to detect and report on maven-shade-plugin configurations and their alignment.
162+
* When enabled, the plugin will analyze shade configurations across all projects and provide additional
163+
* reporting on shaded dependencies.
164+
*/
165+
@Parameter(property = "analyzeShade", defaultValue = "false")
166+
private boolean analyzeShade;
167+
168+
/**
169+
* Print detailed shade configuration information in the report.
170+
* Only has effect when analyzeShade=true. When enabled, shows all shade plugin configurations
171+
* found in the project with their relocation mappings.
172+
*/
173+
@Parameter(property = "printShadeConfigurations", defaultValue = "false")
174+
private boolean printShadeConfigurations;
175+
176+
/**
177+
* Include transitive dependencies in shade analysis.
178+
* Only has effect when analyzeShade=true. When enabled, analyzes both direct and transitive
179+
* dependencies for shade impact, providing comprehensive coverage of all artifacts that
180+
* could be affected by shading configurations.
181+
*/
182+
@Parameter(property = "includeTransitiveShaded", defaultValue = "false")
183+
private boolean includeTransitiveShaded;
184+
149185
private ArtifactFilter scopeFilter;
150186

151187
private static void write(final String string, final File file) throws IOException {
@@ -224,6 +260,37 @@ public void execute() throws MojoExecutionException, MojoFailureException {
224260
String unalignedTransitivesStr = reportUnalignedTransitiveDependenciesSummary(unalignedTransitives);
225261
String unalignedTransitiveDetail = reportUnalignedTransitiveDependencyDetail(alignedDirectDeps, excludeDependencyFilter);
226262

263+
String shadeReport = "";
264+
String shadeAlignmentSummary = "";
265+
266+
if (analyzeShade) {
267+
ShadeAwareAlignmentReporter shadeReporter = new ShadeAwareAlignmentReporter(getLog());
268+
269+
// Collect artifacts for shade analysis
270+
List<Artifact> artifactsForShadeAnalysis;
271+
List<ArtifactWithPath> artifactsWithPathsForShadeAnalysis = null;
272+
273+
if (includeTransitiveShaded) {
274+
artifactsWithPathsForShadeAnalysis = getAllTransitiveDependencyArtifactsWithPaths(directDependencies);
275+
artifactsForShadeAnalysis = artifactsWithPathsForShadeAnalysis.stream()
276+
.map(ArtifactWithPath::getArtifact)
277+
.collect(Collectors.toList());
278+
} else {
279+
artifactsForShadeAnalysis = new ArrayList<>(dependencyArtifacts);
280+
}
281+
282+
ShadeAwareAlignmentReporter.ShadeAlignmentResult shadeResult =
283+
shadeReporter.analyzeShadeAlignment(getReactorProjectsForShadeAnalysis(),
284+
artifactsForShadeAnalysis,
285+
alignmentPattern,
286+
artifactsWithPathsForShadeAnalysis);
287+
288+
if (printShadeConfigurations) {
289+
shadeReport = shadeReporter.generateShadeReport(shadeResult, alignmentPattern);
290+
}
291+
shadeAlignmentSummary = shadeReporter.generateShadeAlignmentSummary(shadeResult, alignmentPattern);
292+
}
293+
227294
if (outputFile != null) {
228295
String projectTitle = getProjectTitle();
229296

@@ -233,13 +300,27 @@ public void execute() throws MojoExecutionException, MojoFailureException {
233300
write(unalignedTransitivesStr, outputFile);
234301
write(unalignedTransitiveDetail, outputFile);
235302

303+
if (analyzeShade && !shadeReport.isEmpty()) {
304+
write(shadeReport, outputFile);
305+
}
306+
if (analyzeShade && !shadeAlignmentSummary.isEmpty()) {
307+
write(shadeAlignmentSummary, outputFile);
308+
}
309+
236310
getLog().info(String.format("Wrote alignment report tree to: %s",
237311
outputFile));
238312
} else {
239313
log(alignedDirectStr, getLog());
240314
log(unalignedDirectStr, getLog());
241315
log(unalignedTransitivesStr, getLog());
242316
log(unalignedTransitiveDetail, getLog());
317+
318+
if (analyzeShade && !shadeReport.isEmpty()) {
319+
log(shadeReport, getLog());
320+
}
321+
if (analyzeShade && !shadeAlignmentSummary.isEmpty()) {
322+
log(shadeAlignmentSummary, getLog());
323+
}
243324
}
244325
} catch (IOException exception) {
245326
throw new MojoExecutionException(
@@ -558,4 +639,121 @@ private ArtifactFilter createExcludeFilter() {
558639
}
559640
return filter;
560641
}
642+
643+
/**
644+
* Filters reactor projects based on the excludeModules parameter.
645+
*
646+
* @param projects the list of projects to filter
647+
* @return the filtered list of projects
648+
*/
649+
protected List<MavenProject> filterModules(final List<MavenProject> projects) {
650+
if (excludeModules == null || excludeModules.trim()
651+
.isEmpty()) {
652+
return projects;
653+
}
654+
655+
List<String> patterns = Arrays.asList(excludeModules.split(","));
656+
getLog().debug(String.format("+ Filtering modules by exclude patterns: %s", patterns));
657+
658+
return projects.stream()
659+
.filter(project -> {
660+
String artifactId = project.getArtifactId();
661+
for (String pattern : patterns) {
662+
String trimmedPattern = pattern.trim();
663+
if (matchesPattern(artifactId, trimmedPattern)) {
664+
getLog().debug(String.format("+ Excluding module: %s (matched pattern: %s)", artifactId, trimmedPattern));
665+
return false;
666+
}
667+
}
668+
return true;
669+
})
670+
.collect(Collectors.toList());
671+
}
672+
673+
/**
674+
* Checks if a string matches a pattern with wildcard support.
675+
*
676+
* @param text the text to match
677+
* @param pattern the pattern (supports * wildcards)
678+
* @return true if the text matches the pattern
679+
*/
680+
private boolean matchesPattern(final String text, final String pattern) {
681+
if (pattern.equals("*")) {
682+
return true;
683+
}
684+
685+
String regex = pattern.replace("*", ".*");
686+
return text.matches(regex);
687+
}
688+
689+
/**
690+
* Gets the reactor projects to use for shade analysis.
691+
* In single-module mode, returns only the current project.
692+
* In aggregate mode, returns filtered reactor projects.
693+
*
694+
* @return the list of projects for shade analysis
695+
*/
696+
protected List<MavenProject> getReactorProjectsForShadeAnalysis() {
697+
return filterModules(reactorProjects);
698+
}
699+
700+
/**
701+
* Collects all transitive dependency artifacts from the given direct dependency nodes.
702+
* This method traverses the entire dependency tree and collects all artifacts at all levels.
703+
*
704+
* @param directDependencies the set of direct dependency nodes to traverse
705+
* @return a list of all artifacts (direct + transitive)
706+
*/
707+
private List<Artifact> getAllTransitiveDependencyArtifacts(final Set<DependencyNode> directDependencies) {
708+
List<ArtifactWithPath> allArtifactsWithPaths = getAllTransitiveDependencyArtifactsWithPaths(directDependencies);
709+
710+
List<Artifact> artifacts = allArtifactsWithPaths.stream()
711+
.map(ArtifactWithPath::getArtifact)
712+
.collect(Collectors.toList());
713+
714+
getLog().debug(String.format("Collected %d total artifacts (direct + transitive) for shade analysis", artifacts.size()));
715+
716+
return artifacts;
717+
}
718+
719+
/**
720+
* Collects all transitive dependency artifacts with their dependency paths.
721+
*
722+
* @param directDependencies the set of direct dependency nodes to traverse
723+
* @return a list of all artifacts with their paths (direct + transitive)
724+
*/
725+
private List<ArtifactWithPath> getAllTransitiveDependencyArtifactsWithPaths(final Set<DependencyNode> directDependencies) {
726+
Set<ArtifactWithPath> allArtifacts = new HashSet<>();
727+
728+
for (DependencyNode directDependency : directDependencies) {
729+
collectAllArtifactsWithPathsFromNode(directDependency, new ArrayList<>(), allArtifacts);
730+
}
731+
732+
getLog().debug(String.format("Collected %d total artifacts with paths for shade analysis", allArtifacts.size()));
733+
734+
return new ArrayList<>(allArtifacts);
735+
}
736+
737+
/**
738+
* Recursively collects all artifacts with their paths from a dependency node and its children.
739+
*
740+
* @param node the dependency node to traverse
741+
* @param currentPath the current dependency path (not including this node)
742+
* @param collector the set to collect artifacts with paths into
743+
*/
744+
private void collectAllArtifactsWithPathsFromNode(final DependencyNode node,
745+
final List<Artifact> currentPath,
746+
final Set<ArtifactWithPath> collector) {
747+
// Create the path including this node
748+
List<Artifact> pathWithThisNode = new ArrayList<>(currentPath);
749+
pathWithThisNode.add(node.getArtifact());
750+
751+
// Add the current node's artifact with its path
752+
collector.add(new ArtifactWithPath(node.getArtifact(), pathWithThisNode));
753+
754+
// Recursively process all children
755+
for (DependencyNode child : node.getChildren()) {
756+
collectAllArtifactsWithPathsFromNode(child, pathWithThisNode, collector);
757+
}
758+
}
561759
}

src/main/java/com/github/streamshub/AggregateAlignmentReporterMojo.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package com.github.streamshub;
2121

2222
import java.util.HashSet;
23+
import java.util.List;
2324
import java.util.Set;
2425

2526
import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
@@ -35,7 +36,9 @@ public class AggregateAlignmentReporterMojo extends AbstractAlignmentReporterMoj
3536
protected Set<DependencyNode> getDirectDependencies(final ArtifactFilter artifactFilter) throws MojoExecutionException {
3637
Set<DependencyNode> dependencies = new HashSet<>();
3738

38-
for (MavenProject reactorProject : reactorProjects) {
39+
List<MavenProject> filteredProjects = filterModules(reactorProjects);
40+
41+
for (MavenProject reactorProject : filteredProjects) {
3942
dependencies.addAll(getDirectDependencies(reactorProject, artifactFilter));
4043
}
4144

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package com.github.streamshub;
21+
22+
import java.util.List;
23+
import java.util.Objects;
24+
25+
import org.apache.maven.artifact.Artifact;
26+
27+
/**
28+
* Represents an artifact along with its dependency path.
29+
* Used for tracking how transitive dependencies are reached in the dependency tree.
30+
*/
31+
public final class ArtifactWithPath {
32+
private final Artifact artifact;
33+
private final List<Artifact> dependencyPath;
34+
private final boolean isDirect;
35+
36+
/**
37+
* Creates an ArtifactWithPath for a direct dependency.
38+
*
39+
* @param artifact the artifact
40+
*/
41+
public ArtifactWithPath(final Artifact artifactParam) {
42+
this.artifact = artifactParam;
43+
this.dependencyPath = List.of(artifactParam);
44+
this.isDirect = true;
45+
}
46+
47+
/**
48+
* Creates an ArtifactWithPath for a transitive dependency.
49+
*
50+
* @param artifact the artifact
51+
* @param dependencyPath the full path from root to this artifact (including this artifact)
52+
*/
53+
public ArtifactWithPath(final Artifact artifactParam, final List<Artifact> pathParam) {
54+
this.artifact = artifactParam;
55+
this.dependencyPath = List.copyOf(pathParam);
56+
this.isDirect = pathParam.size() == 1;
57+
}
58+
59+
public Artifact getArtifact() {
60+
return artifact;
61+
}
62+
63+
public List<Artifact> getDependencyPath() {
64+
return dependencyPath;
65+
}
66+
67+
public boolean isDirect() {
68+
return isDirect;
69+
}
70+
71+
public boolean isTransitive() {
72+
return !isDirect;
73+
}
74+
75+
/**
76+
* Returns the root dependency (the direct dependency that brought this artifact in).
77+
* For direct dependencies, returns the artifact itself.
78+
*/
79+
public Artifact getRootDependency() {
80+
return dependencyPath.get(0);
81+
}
82+
83+
/**
84+
* Formats the dependency path as a string in the format:
85+
* "artifact <- parent <- ... <- root"
86+
*/
87+
public String formatDependencyPath() {
88+
if (isDirect) {
89+
return artifact.toString() + " (direct)";
90+
}
91+
92+
StringBuilder sb = new StringBuilder();
93+
94+
// Add the current artifact first
95+
sb.append(artifact.toString());
96+
97+
// Add the path in reverse order (excluding the current artifact)
98+
for (int i = dependencyPath.size() - 2; i >= 0; i--) {
99+
sb.append(" <- ");
100+
sb.append(dependencyPath.get(i)
101+
.toString());
102+
}
103+
104+
return sb.toString();
105+
}
106+
107+
@Override
108+
public boolean equals(final Object o) {
109+
if (this == o) {
110+
return true;
111+
}
112+
if (o == null || getClass() != o.getClass()) {
113+
return false;
114+
}
115+
ArtifactWithPath that = (ArtifactWithPath) o;
116+
return Objects.equals(artifact, that.artifact);
117+
}
118+
119+
@Override
120+
public int hashCode() {
121+
return Objects.hash(artifact);
122+
}
123+
124+
@Override
125+
public String toString() {
126+
return formatDependencyPath();
127+
}
128+
}

0 commit comments

Comments
 (0)