Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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);
}
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