Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ out/
/bootstrap
/dependencies.xml
.java-version
logs
6 changes: 5 additions & 1 deletion checkstyle/checkstyle-suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ under the License.
<!-- Allow magic numbers in Maven plugin parameter defaults -->
<suppress checks="MagicNumber" files=".*Mojo\.java"/>

<!-- Allow hidden fields for Maven plugin parameters -->
<!-- Allow hidden fields for Maven plugin parameters and constructor parameters -->
<suppress checks="HiddenField" files=".*Mojo\.java"/>
<suppress checks="HiddenField" files=".*\.java"/>

<!-- Allow single-line if statements without braces -->
<suppress checks="NeedBraces" files=".*\.java"/>

<!-- Allow protected/public fields for Maven plugin parameters -->
<suppress checks="VisibilityModifier" files=".*Mojo\.java"/>
Expand Down
198 changes: 198 additions & 0 deletions src/main/java/com/github/streamshub/AbstractAlignmentReporterMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,42 @@ public abstract class AbstractAlignmentReporterMojo extends AbstractMojo {
@Parameter(property = "excludes")
private String excludes;

/**
* A comma-separated list of module names to exclude from processing in multi-module projects, or <code>null</code> to
* include all modules. Each module name pattern supports full and partial <code>*</code> wildcards.
* <p>
* For example, <code>*-test</code> will exclude all modules whose name ends with "-test",
* and <code>flink-*</code> will exclude all modules whose name starts with "flink-".
* </p>
*/
@Parameter(property = "excludeModules")
private String excludeModules;

/**
* Enable shade-aware analysis to detect and report on maven-shade-plugin configurations and their alignment.
* When enabled, the plugin will analyze shade configurations across all projects and provide additional
* reporting on shaded dependencies.
*/
@Parameter(property = "analyzeShade", defaultValue = "false")
private boolean analyzeShade;

/**
* Print detailed shade configuration information in the report.
* Only has effect when analyzeShade=true. When enabled, shows all shade plugin configurations
* found in the project with their relocation mappings.
*/
@Parameter(property = "printShadeConfigurations", defaultValue = "false")
private boolean printShadeConfigurations;

/**
* Include transitive dependencies in shade analysis.
* Only has effect when analyzeShade=true. When enabled, analyzes both direct and transitive
* dependencies for shade impact, providing comprehensive coverage of all artifacts that
* could be affected by shading configurations.
*/
@Parameter(property = "includeTransitiveShaded", defaultValue = "false")
private boolean includeTransitiveShaded;

private ArtifactFilter scopeFilter;

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

String shadeReport = "";
String shadeAlignmentSummary = "";

if (analyzeShade) {
ShadeAwareAlignmentReporter shadeReporter = new ShadeAwareAlignmentReporter(getLog());

// Collect artifacts for shade analysis
List<Artifact> artifactsForShadeAnalysis;
List<ArtifactWithPath> artifactsWithPathsForShadeAnalysis = null;

if (includeTransitiveShaded) {
artifactsWithPathsForShadeAnalysis = getAllTransitiveDependencyArtifactsWithPaths(directDependencies);
artifactsForShadeAnalysis = artifactsWithPathsForShadeAnalysis.stream()
.map(ArtifactWithPath::getArtifact)
.collect(Collectors.toList());
} else {
artifactsForShadeAnalysis = new ArrayList<>(dependencyArtifacts);
}

ShadeAwareAlignmentReporter.ShadeAlignmentResult shadeResult =
shadeReporter.analyzeShadeAlignment(getReactorProjectsForShadeAnalysis(),
artifactsForShadeAnalysis,
alignmentPattern,
artifactsWithPathsForShadeAnalysis);

if (printShadeConfigurations) {
shadeReport = shadeReporter.generateShadeReport(shadeResult, alignmentPattern);
}
shadeAlignmentSummary = shadeReporter.generateShadeAlignmentSummary(shadeResult, alignmentPattern);
}

if (outputFile != null) {
String projectTitle = getProjectTitle();

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

if (analyzeShade && !shadeReport.isEmpty()) {
write(shadeReport, outputFile);
}
if (analyzeShade && !shadeAlignmentSummary.isEmpty()) {
write(shadeAlignmentSummary, outputFile);
}

getLog().info(String.format("Wrote alignment report tree to: %s",
outputFile));
} else {
log(alignedDirectStr, getLog());
log(unalignedDirectStr, getLog());
log(unalignedTransitivesStr, getLog());
log(unalignedTransitiveDetail, getLog());

if (analyzeShade && !shadeReport.isEmpty()) {
log(shadeReport, getLog());
}
if (analyzeShade && !shadeAlignmentSummary.isEmpty()) {
log(shadeAlignmentSummary, getLog());
}
}
} catch (IOException exception) {
throw new MojoExecutionException(
Expand Down Expand Up @@ -558,4 +639,121 @@ private ArtifactFilter createExcludeFilter() {
}
return filter;
}

/**
* Filters reactor projects based on the excludeModules parameter.
*
* @param projects the list of projects to filter
* @return the filtered list of projects
*/
protected List<MavenProject> filterModules(final List<MavenProject> projects) {
if (excludeModules == null || excludeModules.trim()
.isEmpty()) {
return projects;
}

List<String> patterns = Arrays.asList(excludeModules.split(","));
getLog().debug(String.format("+ Filtering modules by exclude patterns: %s", patterns));

return projects.stream()
.filter(project -> {
String artifactId = project.getArtifactId();
for (String pattern : patterns) {
String trimmedPattern = pattern.trim();
if (matchesPattern(artifactId, trimmedPattern)) {
getLog().debug(String.format("+ Excluding module: %s (matched pattern: %s)", artifactId, trimmedPattern));
return false;
}
}
return true;
})
.collect(Collectors.toList());
}

/**
* Checks if a string matches a pattern with wildcard support.
*
* @param text the text to match
* @param pattern the pattern (supports * wildcards)
* @return true if the text matches the pattern
*/
private boolean matchesPattern(final String text, final String pattern) {
if (pattern.equals("*")) {
return true;
}

String regex = pattern.replace("*", ".*");
return text.matches(regex);
}

/**
* Gets the reactor projects to use for shade analysis.
* In single-module mode, returns only the current project.
* In aggregate mode, returns filtered reactor projects.
*
* @return the list of projects for shade analysis
*/
protected List<MavenProject> getReactorProjectsForShadeAnalysis() {
return filterModules(reactorProjects);
}

/**
* Collects all transitive dependency artifacts from the given direct dependency nodes.
* This method traverses the entire dependency tree and collects all artifacts at all levels.
*
* @param directDependencies the set of direct dependency nodes to traverse
* @return a list of all artifacts (direct + transitive)
*/
private List<Artifact> getAllTransitiveDependencyArtifacts(final Set<DependencyNode> directDependencies) {
List<ArtifactWithPath> allArtifactsWithPaths = getAllTransitiveDependencyArtifactsWithPaths(directDependencies);

List<Artifact> artifacts = allArtifactsWithPaths.stream()
.map(ArtifactWithPath::getArtifact)
.collect(Collectors.toList());

getLog().debug(String.format("Collected %d total artifacts (direct + transitive) for shade analysis", artifacts.size()));

return artifacts;
}

/**
* Collects all transitive dependency artifacts with their dependency paths.
*
* @param directDependencies the set of direct dependency nodes to traverse
* @return a list of all artifacts with their paths (direct + transitive)
*/
private List<ArtifactWithPath> getAllTransitiveDependencyArtifactsWithPaths(final Set<DependencyNode> directDependencies) {
Set<ArtifactWithPath> allArtifacts = new HashSet<>();

for (DependencyNode directDependency : directDependencies) {
collectAllArtifactsWithPathsFromNode(directDependency, new ArrayList<>(), allArtifacts);
}

getLog().debug(String.format("Collected %d total artifacts with paths for shade analysis", allArtifacts.size()));

return new ArrayList<>(allArtifacts);
}

/**
* Recursively collects all artifacts with their paths from a dependency node and its children.
*
* @param node the dependency node to traverse
* @param currentPath the current dependency path (not including this node)
* @param collector the set to collect artifacts with paths into
*/
private void collectAllArtifactsWithPathsFromNode(final DependencyNode node,
final List<Artifact> currentPath,
final Set<ArtifactWithPath> collector) {
// Create the path including this node
List<Artifact> pathWithThisNode = new ArrayList<>(currentPath);
pathWithThisNode.add(node.getArtifact());

// Add the current node's artifact with its path
collector.add(new ArtifactWithPath(node.getArtifact(), pathWithThisNode));

// Recursively process all children
for (DependencyNode child : node.getChildren()) {
collectAllArtifactsWithPathsFromNode(child, pathWithThisNode, collector);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package com.github.streamshub;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

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

for (MavenProject reactorProject : reactorProjects) {
List<MavenProject> filteredProjects = filterModules(reactorProjects);

for (MavenProject reactorProject : filteredProjects) {
dependencies.addAll(getDirectDependencies(reactorProject, artifactFilter));
}

Expand Down
128 changes: 128 additions & 0 deletions src/main/java/com/github/streamshub/ArtifactWithPath.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* 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 com.github.streamshub;

import java.util.List;
import java.util.Objects;

import org.apache.maven.artifact.Artifact;

/**
* Represents an artifact along with its dependency path.
* Used for tracking how transitive dependencies are reached in the dependency tree.
*/
public final class ArtifactWithPath {
private final Artifact artifact;
private final List<Artifact> dependencyPath;
private final boolean isDirect;

/**
* Creates an ArtifactWithPath for a direct dependency.
*
* @param artifact the artifact
*/
public ArtifactWithPath(final Artifact artifactParam) {
this.artifact = artifactParam;
this.dependencyPath = List.of(artifactParam);
this.isDirect = true;
}

/**
* Creates an ArtifactWithPath for a transitive dependency.
*
* @param artifact the artifact
* @param dependencyPath the full path from root to this artifact (including this artifact)
*/
public ArtifactWithPath(final Artifact artifactParam, final List<Artifact> pathParam) {
this.artifact = artifactParam;
this.dependencyPath = List.copyOf(pathParam);
this.isDirect = pathParam.size() == 1;
}

public Artifact getArtifact() {
return artifact;
}

public List<Artifact> getDependencyPath() {
return dependencyPath;
}

public boolean isDirect() {
return isDirect;
}

public boolean isTransitive() {
return !isDirect;
}

/**
* Returns the root dependency (the direct dependency that brought this artifact in).
* For direct dependencies, returns the artifact itself.
*/
public Artifact getRootDependency() {
return dependencyPath.get(0);
}

/**
* Formats the dependency path as a string in the format:
* "artifact <- parent <- ... <- root"
*/
public String formatDependencyPath() {
if (isDirect) {
return artifact.toString() + " (direct)";
}

StringBuilder sb = new StringBuilder();

// Add the current artifact first
sb.append(artifact.toString());

// Add the path in reverse order (excluding the current artifact)
for (int i = dependencyPath.size() - 2; i >= 0; i--) {
sb.append(" <- ");
sb.append(dependencyPath.get(i)
.toString());
}

return sb.toString();
}

@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ArtifactWithPath that = (ArtifactWithPath) o;
return Objects.equals(artifact, that.artifact);
}

@Override
public int hashCode() {
return Objects.hash(artifact);
}

@Override
public String toString() {
return formatDependencyPath();
}
}
Loading