diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/AddAnnotationProcessor.java b/rewrite-maven/src/main/java/org/openrewrite/maven/AddAnnotationProcessor.java index 8e503f4394a..2f0bd279e7e 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/AddAnnotationProcessor.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/AddAnnotationProcessor.java @@ -15,6 +15,7 @@ */ package org.openrewrite.maven; +import com.fasterxml.jackson.databind.JsonNode; import lombok.EqualsAndHashCode; import lombok.Value; import org.jspecify.annotations.Nullable; @@ -22,6 +23,9 @@ import org.openrewrite.internal.ListUtils; import org.openrewrite.maven.trait.MavenPlugin; import org.openrewrite.maven.tree.MavenResolutionResult; +import org.openrewrite.maven.tree.Plugin; +import org.openrewrite.maven.tree.Pom; +import org.openrewrite.maven.tree.ResolvedPom; import org.openrewrite.semver.Semver; import org.openrewrite.semver.VersionComparator; import org.openrewrite.xml.XmlIsoVisitor; @@ -94,6 +98,13 @@ public static class Scanned { */ Set aggregatorPaths = new HashSet<>(); + /** + * Paths of POMs where the annotation processor is already present in the effective POM + * (via a parent POM outside the reactor), but not in the POM's own XML. + * These POMs should be skipped to avoid redundant configuration. + */ + Set alreadyConfiguredInEffectivePomPaths = new HashSet<>(); + /** * Get the actual orphan paths (candidates minus aggregator-only POMs). * A true orphan has no parent in reactor and is not an aggregator-only POM. @@ -121,7 +132,20 @@ public TreeVisitor getScanner(Scanned acc) { @Override public Xml.Document visitDocument(Xml.Document document, ExecutionContext ctx) { MavenResolutionResult mrr = getResolutionResult(); - Path sourcePath = mrr.getPom().getRequested().getSourcePath(); + ResolvedPom resolvedPom = mrr.getPom(); + Path sourcePath = resolvedPom.getRequested().getSourcePath(); + + // Check if the annotation processor is already in the effective POM (merged from parent POMs) + // but NOT in the current POM's own XML. In that case, skip this POM to avoid adding + // redundant configuration that duplicates what the parent already provides. + boolean inEffectivePom = hasAnnotationProcessor(resolvedPom.getPlugins()) || + hasAnnotationProcessor(resolvedPom.getPluginManagement()); + Pom requestedPom = resolvedPom.getRequested(); + boolean inCurrentPomXml = hasAnnotationProcessor(requestedPom.getPlugins()) || + hasAnnotationProcessor(requestedPom.getPluginManagement()); + if (sourcePath != null && inEffectivePom && !inCurrentPomXml) { + acc.alreadyConfiguredInEffectivePomPaths.add(sourcePath); + } if (mrr.parentPomIsProjectPom()) { // This module has a parent within the reactor @@ -177,6 +201,12 @@ public TreeVisitor getVisitor(Scanned acc) { return tree; } + // Skip POMs where the annotation processor is already configured via a parent POM + // outside the reactor (present in effective POM but not in own XML) + if (acc.alreadyConfiguredInEffectivePomPaths.contains(sourcePath)) { + return tree; + } + // First, ensure the plugin exists - use the source path as file pattern tree = new AddPluginVisitor(isParent, MAVEN_COMPILER_PLUGIN_GROUP_ID, MAVEN_COMPILER_PLUGIN_ARTIFACT_ID, null, @@ -254,4 +284,33 @@ public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) { } }; } + + private boolean hasAnnotationProcessor(List plugins) { + for (Plugin plugin : plugins) { + if (!MAVEN_COMPILER_PLUGIN_GROUP_ID.equals(plugin.getGroupId()) || + !MAVEN_COMPILER_PLUGIN_ARTIFACT_ID.equals(plugin.getArtifactId())) { + continue; + } + JsonNode config = plugin.getConfiguration(); + if (config == null || config.isMissingNode()) { + continue; + } + JsonNode paths = config.path("annotationProcessorPaths").path("path"); + if (paths.isArray()) { + for (JsonNode path : paths) { + if (isMatchingProcessor(path)) { + return true; + } + } + } else if (!paths.isMissingNode() && isMatchingProcessor(paths)) { + return true; + } + } + return false; + } + + private boolean isMatchingProcessor(JsonNode path) { + return groupId.equals(path.path("groupId").asText("")) && + artifactId.equals(path.path("artifactId").asText("")); + } } diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/AddAnnotationProcessorTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/AddAnnotationProcessorTest.java index 9c247e7a717..2df60cbac35 100644 --- a/rewrite-maven/src/test/java/org/openrewrite/maven/AddAnnotationProcessorTest.java +++ b/rewrite-maven/src/test/java/org/openrewrite/maven/AddAnnotationProcessorTest.java @@ -597,6 +597,89 @@ void addToBuildPluginsWhenInBothLocations() { } } + @Nested + class EffectivePomCheck { + + @Test + void doesNotAddWhenAlreadyInEffectivePomViaAncestor() { + // 3-level hierarchy: grandparent has the processor in pluginManagement, + // intermediate parent inherits it via effective POM but has no own XML config. + // Expected: intermediate parent is NOT modified (annotation processor already in effective POM) + rewriteRun( + pomXml( + // Grandparent already has the processor configured in pluginManagement + """ + + 4.0.0 + com.mycompany.app + grandparent + 1 + pom + + parent-module + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + + + + + + + """ + ), + mavenProject("parent-module", + pomXml( + // Intermediate parent: inherits the processor from grandparent via effective POM, + // but does NOT have it in its own XML. Should not be modified. + """ + + 4.0.0 + + com.mycompany.app + grandparent + 1 + + parent-module + pom + + child-module + + + """ + ) + ), + mavenProject("child-module", + pomXml( + """ + + 4.0.0 + + com.mycompany.app + parent-module + 1 + + child-module + + """ + ) + ) + ); + } + } + @Nested class CombinedAggregatorAndParent {