diff --git a/.gitignore b/.gitignore index f79c928..cd8e23d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ out/ /bootstrap /dependencies.xml .java-version +logs \ No newline at end of file diff --git a/checkstyle/checkstyle-suppressions.xml b/checkstyle/checkstyle-suppressions.xml index 5db604e..c7e00fa 100644 --- a/checkstyle/checkstyle-suppressions.xml +++ b/checkstyle/checkstyle-suppressions.xml @@ -31,8 +31,12 @@ under the License. - + + + + + diff --git a/src/main/java/com/github/streamshub/AbstractAlignmentReporterMojo.java b/src/main/java/com/github/streamshub/AbstractAlignmentReporterMojo.java index 51fdec0..1ff6fd3 100644 --- a/src/main/java/com/github/streamshub/AbstractAlignmentReporterMojo.java +++ b/src/main/java/com/github/streamshub/AbstractAlignmentReporterMojo.java @@ -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 null to + * include all modules. Each module name pattern supports full and partial * wildcards. + *

+ * For example, *-test will exclude all modules whose name ends with "-test", + * and flink-* will exclude all modules whose name starts with "flink-". + *

+ */ + @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 { @@ -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 artifactsForShadeAnalysis; + List 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(); @@ -233,6 +300,13 @@ 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 { @@ -240,6 +314,13 @@ public void execute() throws MojoExecutionException, MojoFailureException { 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( @@ -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 filterModules(final List projects) { + if (excludeModules == null || excludeModules.trim() + .isEmpty()) { + return projects; + } + + List 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 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 getAllTransitiveDependencyArtifacts(final Set directDependencies) { + List allArtifactsWithPaths = getAllTransitiveDependencyArtifactsWithPaths(directDependencies); + + List 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 getAllTransitiveDependencyArtifactsWithPaths(final Set directDependencies) { + Set 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 currentPath, + final Set collector) { + // Create the path including this node + List 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); + } + } } diff --git a/src/main/java/com/github/streamshub/AggregateAlignmentReporterMojo.java b/src/main/java/com/github/streamshub/AggregateAlignmentReporterMojo.java index 2b0428f..e2371bd 100644 --- a/src/main/java/com/github/streamshub/AggregateAlignmentReporterMojo.java +++ b/src/main/java/com/github/streamshub/AggregateAlignmentReporterMojo.java @@ -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; @@ -35,7 +36,9 @@ public class AggregateAlignmentReporterMojo extends AbstractAlignmentReporterMoj protected Set getDirectDependencies(final ArtifactFilter artifactFilter) throws MojoExecutionException { Set dependencies = new HashSet<>(); - for (MavenProject reactorProject : reactorProjects) { + List filteredProjects = filterModules(reactorProjects); + + for (MavenProject reactorProject : filteredProjects) { dependencies.addAll(getDirectDependencies(reactorProject, artifactFilter)); } diff --git a/src/main/java/com/github/streamshub/ArtifactWithPath.java b/src/main/java/com/github/streamshub/ArtifactWithPath.java new file mode 100644 index 0000000..35481c8 --- /dev/null +++ b/src/main/java/com/github/streamshub/ArtifactWithPath.java @@ -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 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 pathParam) { + this.artifact = artifactParam; + this.dependencyPath = List.copyOf(pathParam); + this.isDirect = pathParam.size() == 1; + } + + public Artifact getArtifact() { + return artifact; + } + + public List 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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/streamshub/ShadeAnalyzer.java b/src/main/java/com/github/streamshub/ShadeAnalyzer.java new file mode 100644 index 0000000..d948c9e --- /dev/null +++ b/src/main/java/com/github/streamshub/ShadeAnalyzer.java @@ -0,0 +1,244 @@ +/* + * 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.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.apache.maven.model.Plugin; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.util.xml.Xpp3Dom; + +public final class ShadeAnalyzer { + + public static final class ShadeRelocation { + private final String pattern; + private final String shadedPattern; + + public ShadeRelocation(final String pattern, final String shadedPattern) { + this.pattern = pattern; + this.shadedPattern = shadedPattern; + } + + public String getPattern() { + return pattern; + } + + public String getShadedPattern() { + return shadedPattern; + } + + public String applyRelocation(final String originalPackage) { + if (originalPackage.startsWith(pattern)) { + return originalPackage.replace(pattern, shadedPattern); + } + return originalPackage; + } + + @Override + public String toString() { + return String.format("%s -> %s", pattern, shadedPattern); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ShadeRelocation that = (ShadeRelocation) o; + return Objects.equals(pattern, that.pattern) && Objects.equals(shadedPattern, that.shadedPattern); + } + + @Override + public int hashCode() { + return Objects.hash(pattern, shadedPattern); + } + } + + public static final class ShadeConfiguration { + private final List relocations; + private final boolean createDependencyReducedPom; + private final String moduleName; + + public ShadeConfiguration(final List relocations, + final boolean createDependencyReducedPom, + final String moduleName) { + this.relocations = relocations != null ? relocations : new ArrayList<>(); + this.createDependencyReducedPom = createDependencyReducedPom; + this.moduleName = moduleName; + } + + public List getRelocations() { + return relocations; + } + + public boolean isCreateDependencyReducedPom() { + return createDependencyReducedPom; + } + + public boolean hasRelocations() { + return !relocations.isEmpty(); + } + + public String getModuleName() { + return moduleName; + } + } + + private final Log log; + + public ShadeAnalyzer(final Log log) { + this.log = log; + } + + public ShadeConfiguration analyzeShadeConfiguration(final MavenProject project) { + Plugin shadePlugin = findShadePlugin(project); + if (shadePlugin == null) { + log.debug(String.format("No maven-shade-plugin found in project: %s", project.getArtifactId())); + return new ShadeConfiguration(new ArrayList<>(), false, project.getArtifactId()); + } + + return parseShadeConfiguration(shadePlugin, project.getArtifactId()); + } + + public List analyzeAllShadeConfigurations(final List projects) { + List configurations = new ArrayList<>(); + + for (MavenProject project : projects) { + ShadeConfiguration config = analyzeShadeConfiguration(project); + if (config.hasRelocations()) { + configurations.add(config); + log.debug(String.format("Found shade configuration in project: %s with %d relocations", + project.getArtifactId(), config.getRelocations() + .size())); + } + } + + return configurations; + } + + public boolean isArtifactShaded(final String groupId, final String artifactId, final List shadeConfigs) { + // Check both groupId and common package patterns for this artifact + for (ShadeConfiguration config : shadeConfigs) { + for (ShadeRelocation relocation : config.getRelocations()) { + String pattern = relocation.getPattern(); + + // Check if groupId matches the pattern directly + if (groupId.startsWith(pattern)) { + return true; + } + + // Check if this looks like a known mapping between Maven groupId and Java package + if (isKnownGroupIdToPackageMapping(groupId, artifactId, pattern)) { + return true; + } + } + } + + return false; + } + + /** + * Checks for known mappings between Maven groupId and Java package names. + * This handles cases where the Maven groupId differs from the Java package prefix. + */ + private boolean isKnownGroupIdToPackageMapping(final String groupId, final String artifactId, final String packagePattern) { + // Handle cases where groupId is same as package (most common) + if (packagePattern.startsWith(groupId)) { + return true; + } + + // Handle cases where the package pattern contains the artifactId + return packagePattern.contains(artifactId); + } + + + private Plugin findShadePlugin(final MavenProject project) { + if (project.getBuild() == null || project.getBuild() + .getPlugins() == null) { + return null; + } + + return project.getBuild() + .getPlugins() + .stream() + .filter(plugin -> "org.apache.maven.plugins".equals(plugin.getGroupId()) + && "maven-shade-plugin".equals(plugin.getArtifactId())) + .findFirst() + .orElse(null); + } + + private ShadeConfiguration parseShadeConfiguration(final Plugin shadePlugin, final String moduleName) { + List relocations = new ArrayList<>(); + boolean createDependencyReducedPom = false; + + Xpp3Dom configuration = (Xpp3Dom) shadePlugin.getConfiguration(); + if (configuration != null) { + createDependencyReducedPom = parseCreateDependencyReducedPom(configuration); + relocations.addAll(parseRelocations(configuration)); + } + + if (shadePlugin.getExecutions() != null) { + for (org.apache.maven.model.PluginExecution execution : shadePlugin.getExecutions()) { + Xpp3Dom execConfig = (Xpp3Dom) execution.getConfiguration(); + if (execConfig != null) { + relocations.addAll(parseRelocations(execConfig)); + } + } + } + + return new ShadeConfiguration(relocations, createDependencyReducedPom, moduleName); + } + + private boolean parseCreateDependencyReducedPom(final Xpp3Dom configuration) { + Xpp3Dom createReducedPom = configuration.getChild("createDependencyReducedPom"); + if (createReducedPom != null) { + return Boolean.parseBoolean(createReducedPom.getValue()); + } + return false; + } + + private List parseRelocations(final Xpp3Dom configuration) { + List relocations = new ArrayList<>(); + + Xpp3Dom relocationsNode = configuration.getChild("relocations"); + if (relocationsNode != null) { + Xpp3Dom[] relocationNodes = relocationsNode.getChildren("relocation"); + + for (Xpp3Dom relocationNode : relocationNodes) { + String pattern = getChildValue(relocationNode, "pattern"); + String shadedPattern = getChildValue(relocationNode, "shadedPattern"); + + if (pattern != null && shadedPattern != null) { + relocations.add(new ShadeRelocation(pattern, shadedPattern)); + log.debug(String.format("Found relocation: %s -> %s", pattern, shadedPattern)); + } + } + } + + return relocations; + } + + private String getChildValue(final Xpp3Dom parent, final String childName) { + Xpp3Dom child = parent.getChild(childName); + return child != null ? child.getValue() : null; + } +} \ No newline at end of file diff --git a/src/main/java/com/github/streamshub/ShadeAwareAlignmentReporter.java b/src/main/java/com/github/streamshub/ShadeAwareAlignmentReporter.java new file mode 100644 index 0000000..ea8b7ef --- /dev/null +++ b/src/main/java/com/github/streamshub/ShadeAwareAlignmentReporter.java @@ -0,0 +1,296 @@ +/* + * 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.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Comparator; +import java.util.List; +import java.util.regex.Pattern; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.project.MavenProject; + +public final class ShadeAwareAlignmentReporter { + + public static final class ShadeAlignmentResult { + private final List shadeConfigurations; + private final List shadedArtifacts; + private final List unshadedArtifacts; + private final List shadedArtifactsWithPaths; + + public ShadeAlignmentResult(final List configurations, + final List shaded, + final List unshaded) { + this.shadeConfigurations = configurations; + this.shadedArtifacts = shaded; + this.unshadedArtifacts = unshaded; + this.shadedArtifactsWithPaths = null; + } + + public ShadeAlignmentResult(final List configurations, + final List shaded, + final List unshaded, + final List shadedWithPaths) { + this.shadeConfigurations = configurations; + this.shadedArtifacts = shaded; + this.unshadedArtifacts = unshaded; + this.shadedArtifactsWithPaths = shadedWithPaths; + } + + public List getShadeConfigurations() { + return shadeConfigurations; + } + + public List getShadedArtifacts() { + return shadedArtifacts; + } + + public List getUnshadedArtifacts() { + return unshadedArtifacts; + } + + public boolean hasShadeConfigurations() { + return !shadeConfigurations.isEmpty(); + } + + public List getShadedArtifactsWithPaths() { + return shadedArtifactsWithPaths; + } + + public boolean hasPathInformation() { + return shadedArtifactsWithPaths != null; + } + } + + private final ShadeAnalyzer shadeAnalyzer; + private final Log log; + + public ShadeAwareAlignmentReporter(final Log logger) { + this.log = logger; + this.shadeAnalyzer = new ShadeAnalyzer(log); + } + + public ShadeAlignmentResult analyzeShadeAlignment(final List projects, + final List artifacts, + final Pattern alignmentPattern) { + return analyzeShadeAlignment(projects, artifacts, alignmentPattern, null); + } + + public ShadeAlignmentResult analyzeShadeAlignment(final List projects, + final List artifacts, + final Pattern alignmentPattern, + final List artifactsWithPaths) { + + List shadeConfigs = shadeAnalyzer.analyzeAllShadeConfigurations(projects); + + List shadedArtifacts = artifacts.stream() + .filter(artifact -> shadeAnalyzer.isArtifactShaded(artifact.getGroupId(), artifact.getArtifactId(), shadeConfigs)) + .collect(java.util.stream.Collectors.toList()); + + List unshadedArtifacts = artifacts.stream() + .filter(artifact -> !shadeAnalyzer.isArtifactShaded(artifact.getGroupId(), artifact.getArtifactId(), shadeConfigs)) + .collect(java.util.stream.Collectors.toList()); + + // Filter artifactsWithPaths to only include shaded ones + List shadedArtifactsWithPaths = null; + if (artifactsWithPaths != null) { + shadedArtifactsWithPaths = artifactsWithPaths.stream() + .filter(awp -> shadeAnalyzer.isArtifactShaded(awp.getArtifact() + .getGroupId(), awp.getArtifact() + .getArtifactId(), shadeConfigs)) + .collect(java.util.stream.Collectors.toList()); + } + + return new ShadeAlignmentResult(shadeConfigs, shadedArtifacts, unshadedArtifacts, shadedArtifactsWithPaths); + } + + public String generateShadeReport(final ShadeAlignmentResult result, final Pattern alignmentPattern) throws IOException { + try (StringWriter out = new StringWriter(); PrintWriter writer = new PrintWriter(out)) { + + if (!result.hasShadeConfigurations()) { + writer.println("No shade configurations found in the project."); + writer.println(); + return out.toString(); + } + + writer.println("Shade Configuration Analysis"); + writer.println("==========================="); + writer.println(); + + writer.println(String.format("Found %d shade configuration(s)", result.getShadeConfigurations() + .size())); + writer.println(); + + int configIndex = 1; + for (ShadeAnalyzer.ShadeConfiguration config : result.getShadeConfigurations()) { + writer.println(String.format("Configuration #%d (%s):", configIndex++, config.getModuleName())); + writer.println(String.format(" - Create dependency reduced POM: %s", config.isCreateDependencyReducedPom())); + writer.println(String.format(" - Number of relocations: %d", config.getRelocations() + .size())); + + if (!config.getRelocations() + .isEmpty()) { + writer.println(" - Relocations:"); + for (ShadeAnalyzer.ShadeRelocation relocation : config.getRelocations()) { + writer.println(String.format(" * %s", relocation.toString())); + } + } + writer.println(); + } + + // Show both aligned and unaligned shaded artifacts, similar to regular dependency reports + List alignedShadedArtifacts = result.getShadedArtifacts() + .stream() + .filter(artifact -> alignmentPattern.matcher(artifact.getVersion()).find()) + .collect(java.util.stream.Collectors.toList()); + + List unalignedShadedArtifacts = result.getShadedArtifacts() + .stream() + .filter(artifact -> !alignmentPattern.matcher(artifact.getVersion()).find()) + .collect(java.util.stream.Collectors.toList()); + + if (!alignedShadedArtifacts.isEmpty()) { + String title = String.format("%d Aligned artifacts affected by shading", alignedShadedArtifacts.size()); + writer.println(title); + writer.println("-".repeat(title.length())); + for (Artifact artifact : alignedShadedArtifacts) { + writer.println(String.format("Aligned - %s", artifact)); + } + writer.println(); + } + + if (!unalignedShadedArtifacts.isEmpty()) { + String title = String.format("%d Unaligned artifacts affected by shading", unalignedShadedArtifacts.size()); + writer.println(title); + writer.println("-".repeat(title.length())); + for (Artifact artifact : unalignedShadedArtifacts) { + writer.println(String.format("Unaligned - %s", artifact)); + } + writer.println(); + } + + return out.toString(); + } + } + + public String generateShadeAlignmentSummary(final ShadeAlignmentResult result, final Pattern alignmentPattern) throws IOException { + try (StringWriter out = new StringWriter(); PrintWriter writer = new PrintWriter(out)) { + + if (!result.hasShadeConfigurations()) { + return ""; + } + + writer.println("Shade-Aware Alignment Summary"); + writer.println("============================"); + writer.println(); + + long alignedShaded = result.getShadedArtifacts() + .stream() + .filter(artifact -> alignmentPattern.matcher(artifact.getVersion()) + .find()) + .count(); + + long unalignedShaded = result.getShadedArtifacts() + .size() - alignedShaded; + + writer.println(String.format("Shaded artifacts: %d total, %d aligned, %d unaligned", + result.getShadedArtifacts() + .size(), alignedShaded, unalignedShaded)); + + if (unalignedShaded > 0) { + writer.println(); + writer.println("Unaligned Shaded Artifacts:"); + writer.println("---------------------------"); + result.getShadedArtifacts() + .stream() + .filter(artifact -> !alignmentPattern.matcher(artifact.getVersion()) + .find()) + .forEach(artifact -> writer.println(String.format("Unaligned - %s", artifact))); + } + + // Add path information if available + if (result.hasPathInformation() && !result.getShadedArtifactsWithPaths() + .isEmpty()) { + writer.println(); + writer.println("Shaded Artifacts with Dependency Paths"); + writer.println("--------------------------------------"); + + // Group by alignment status and dependency type + List transitiveAlignedShaded = result.getShadedArtifactsWithPaths() + .stream() + .filter(ArtifactWithPath::isTransitive) + .filter(awp -> alignmentPattern.matcher(awp.getArtifact().getVersion()).find()) + .sorted(Comparator.comparing(a -> a.getArtifact() + .toString())) + .collect(java.util.stream.Collectors.toList()); + + List transitiveUnalignedShaded = result.getShadedArtifactsWithPaths() + .stream() + .filter(ArtifactWithPath::isTransitive) + .filter(awp -> !alignmentPattern.matcher(awp.getArtifact().getVersion()).find()) + .sorted(Comparator.comparing(a -> a.getArtifact() + .toString())) + .collect(java.util.stream.Collectors.toList()); + + if (!transitiveAlignedShaded.isEmpty()) { + writer.println(); + writer.println("Direct Aligned Shaded Artifacts:"); + writer.println("---------------------------------"); + for (ArtifactWithPath awp : transitiveAlignedShaded) { + writer.println(String.format("Aligned - %s", awp.getArtifact())); + } + } + + if (!transitiveUnalignedShaded.isEmpty()) { + writer.println(); + writer.println("Direct Unaligned Shaded Artifacts:"); + writer.println("----------------------------------"); + for (ArtifactWithPath awp : transitiveUnalignedShaded) { + writer.println(String.format("Unaligned - %s", awp.getArtifact())); + } + } + + if (!transitiveAlignedShaded.isEmpty()) { + writer.println(); + writer.println("Transitive Aligned Shaded Artifacts:"); + writer.println("------------------------------------"); + for (ArtifactWithPath awp : transitiveAlignedShaded) { + writer.println(String.format("Aligned - %s", awp.formatDependencyPath())); + } + } + + if (!transitiveUnalignedShaded.isEmpty()) { + writer.println(); + writer.println("Transitive Unaligned Shaded Artifacts:"); + writer.println("--------------------------------------"); + for (ArtifactWithPath awp : transitiveUnalignedShaded) { + writer.println(String.format("Unaligned - %s", awp.formatDependencyPath())); + } + } + } + + writer.println(); + return out.toString(); + } + } +} \ No newline at end of file