Skip to content

Commit 29b630b

Browse files
authored
Maven annotation processor and plugin recipes should correctly handle parent and aggregator poms (#6546)
* add test to secure assumptions and document planned behavior of `AddAnnotationProcessor` * Refactor AddAnnotationProcessor tests for single vs multi-module behavior Restructure tests into @nested classes to clearly define expected behavior: - SingleModuleProject: add to build/plugins - MultiModuleProject: add to build/pluginManagement/plugins in root pom only Remove redundant tests, keeping @DocumentExample and version handling tests. * Add recipe to add a managed plugin by reusing the add plugin visitor * Expand MavenPlugin trait to be limitable to managed or build plugins * use AddPlugin, AddManagedPlugin and limitable MavenPlugin trait to add annotation processors where expected * advance `MavenPlugin` trait to also be limitable on groupd and artifact id * add comments and advance tests * update recipes.csv * enhance `AddAnnotationProcessortTest` to reflect handling of separated aggregator and parent poms * update recipe to deal with individual reactor and parent poms * update recipes.csv
1 parent e94d36f commit 29b630b

10 files changed

Lines changed: 3021 additions & 140 deletions

File tree

rewrite-maven/src/main/java/org/openrewrite/maven/AddAnnotationProcessor.java

Lines changed: 184 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,8 @@
1717

1818
import lombok.EqualsAndHashCode;
1919
import lombok.Value;
20-
import org.openrewrite.ExecutionContext;
21-
import org.openrewrite.Option;
22-
import org.openrewrite.Recipe;
23-
import org.openrewrite.TreeVisitor;
20+
import org.jspecify.annotations.Nullable;
21+
import org.openrewrite.*;
2422
import org.openrewrite.internal.ListUtils;
2523
import org.openrewrite.maven.trait.MavenPlugin;
2624
import org.openrewrite.maven.tree.MavenResolutionResult;
@@ -29,12 +27,25 @@
2927
import org.openrewrite.xml.XmlIsoVisitor;
3028
import org.openrewrite.xml.tree.Xml;
3129

30+
import java.nio.file.Path;
31+
import java.util.HashSet;
3232
import java.util.List;
33+
import java.util.Set;
3334
import java.util.concurrent.atomic.AtomicReference;
3435

36+
/**
37+
* Adds an annotation processor to the maven-compiler-plugin configuration.
38+
* <p>
39+
* The behavior differs based on project structure:
40+
* <ul>
41+
* <li><b>Single module:</b> Adds to build/plugins</li>
42+
* <li><b>Multi-module with parent in reactor:</b> Adds to parent's build/pluginManagement/plugins</li>
43+
* <li><b>Orphan module (no parent in reactor):</b> Adds to build/plugins</li>
44+
* </ul>
45+
*/
3546
@Value
3647
@EqualsAndHashCode(callSuper = false)
37-
public class AddAnnotationProcessor extends Recipe {
48+
public class AddAnnotationProcessor extends ScanningRecipe<AddAnnotationProcessor.Scanned> {
3849
private static final String MAVEN_COMPILER_PLUGIN_GROUP_ID = "org.apache.maven.plugins";
3950
private static final String MAVEN_COMPILER_PLUGIN_ARTIFACT_ID = "maven-compiler-plugin";
4051

@@ -56,68 +67,190 @@ public class AddAnnotationProcessor extends Recipe {
5667

5768
String displayName = "Add an annotation processor to `maven-compiler-plugin`";
5869

59-
String description = "Add an annotation processor to the maven compiler plugin. Will not do anything if it already exists. " +
60-
"Also doesn't add anything when no other annotation processors are defined yet. " +
61-
"(Perhaps `ChangePluginConfiguration` can be used).";
70+
String description = "Add an annotation processor path to the `maven-compiler-plugin` configuration. " +
71+
"For modules with an in-reactor parent, adds to the parent's `build/pluginManagement/plugins` section. " +
72+
"For modules without a parent or with a parent outside the reactor, adds directly to `build/plugins`. " +
73+
"Updates the annotation processor version if a newer version is specified.";
74+
75+
/**
76+
* Accumulator to track which POMs need modifications and how.
77+
*/
78+
public static class Scanned {
79+
/**
80+
* Source paths of POMs that are referenced as parents by at least one child within the reactor.
81+
* These should get pluginManagement updates.
82+
*/
83+
Set<Path> parentPomPaths = new HashSet<>();
84+
85+
/**
86+
* Source paths of POMs that have no parent within the reactor.
87+
* After scanning, aggregator-only POMs will be filtered out.
88+
*/
89+
Set<Path> candidateOrphanPaths = new HashSet<>();
90+
91+
/**
92+
* Source paths of POMs that have a &lt;modules&gt; section (aggregators).
93+
* Used to identify aggregator-only POMs that should not be modified.
94+
*/
95+
Set<Path> aggregatorPaths = new HashSet<>();
96+
97+
/**
98+
* Get the actual orphan paths (candidates minus aggregator-only POMs).
99+
* A true orphan has no parent in reactor and is not an aggregator-only POM.
100+
*/
101+
Set<Path> getOrphanPomPaths() {
102+
Set<Path> result = new HashSet<>(candidateOrphanPaths);
103+
// Remove aggregator-only POMs (aggregators that are not also parents)
104+
for (Path aggregatorPath : aggregatorPaths) {
105+
if (!parentPomPaths.contains(aggregatorPath)) {
106+
result.remove(aggregatorPath);
107+
}
108+
}
109+
return result;
110+
}
111+
}
112+
113+
@Override
114+
public Scanned getInitialValue(ExecutionContext ctx) {
115+
return new Scanned();
116+
}
62117

63118
@Override
64-
public TreeVisitor<?, ExecutionContext> getVisitor() {
65-
return new MavenVisitor<ExecutionContext>() {
119+
public TreeVisitor<?, ExecutionContext> getScanner(Scanned acc) {
120+
return new MavenIsoVisitor<ExecutionContext>() {
66121
@Override
67-
public Xml visitTag(Xml.Tag tag, ExecutionContext ctx) {
68-
Xml.Tag plugins = (Xml.Tag) super.visitTag(tag, ctx);
69-
plugins = (Xml.Tag) new MavenPlugin.Matcher().asVisitor(plugin -> {
70-
if (MAVEN_COMPILER_PLUGIN_GROUP_ID.equals(plugin.getGroupId()) &&
71-
MAVEN_COMPILER_PLUGIN_ARTIFACT_ID.equals(plugin.getArtifactId())) {
72-
MavenResolutionResult mrr = getResolutionResult();
73-
AtomicReference<TreeVisitor<?, ExecutionContext>> afterVisitor = new AtomicReference<>();
74-
Xml.Tag modifiedPlugin = new XmlIsoVisitor<ExecutionContext>() {
75-
@Override
76-
public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) {
77-
Xml.Tag tg = super.visitTag(tag, ctx);
78-
if ("annotationProcessorPaths".equals(tg.getName())) {
122+
public Xml.Document visitDocument(Xml.Document document, ExecutionContext ctx) {
123+
MavenResolutionResult mrr = getResolutionResult();
124+
Path sourcePath = mrr.getPom().getRequested().getSourcePath();
125+
126+
if (mrr.parentPomIsProjectPom()) {
127+
// This module has a parent within the reactor
128+
// Mark the parent for pluginManagement update
129+
MavenResolutionResult parent = mrr.getParent();
130+
if (parent != null) {
131+
Path parentPath = parent.getPom().getRequested().getSourcePath();
132+
if (parentPath != null) {
133+
acc.parentPomPaths.add(parentPath);
134+
}
135+
}
136+
} else {
137+
// This module has no parent within the reactor
138+
// Mark as candidate orphan (will be filtered later if it's aggregator-only)
139+
if (sourcePath != null) {
140+
acc.candidateOrphanPaths.add(sourcePath);
141+
}
142+
}
143+
144+
// Track aggregator POMs (those with <modules> section)
145+
List<String> subprojects = mrr.getPom().getSubprojects();
146+
if (sourcePath != null && subprojects != null && !subprojects.isEmpty()) {
147+
acc.aggregatorPaths.add(sourcePath);
148+
}
149+
150+
return document;
151+
}
152+
};
153+
}
154+
155+
@Override
156+
public TreeVisitor<?, ExecutionContext> getVisitor(Scanned acc) {
157+
return new TreeVisitor<Tree, ExecutionContext>() {
158+
@Override
159+
public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) {
160+
if (tree == null) {
161+
return null;
162+
}
163+
164+
MavenResolutionResult mrr = tree.getMarkers().findFirst(MavenResolutionResult.class).orElse(null);
165+
if (mrr == null) {
166+
return tree;
167+
}
168+
169+
Path sourcePath = mrr.getPom().getRequested().getSourcePath();
170+
if (sourcePath == null) {
171+
return tree;
172+
}
173+
174+
boolean isParent = acc.parentPomPaths.contains(sourcePath);
175+
// Skip POMs that are neither parents nor orphans (children with parents in reactor)
176+
if (!isParent && !acc.getOrphanPomPaths().contains(sourcePath)) {
177+
return tree;
178+
}
179+
180+
// First, ensure the plugin exists - use the source path as file pattern
181+
tree = new AddPluginVisitor(isParent,
182+
MAVEN_COMPILER_PLUGIN_GROUP_ID, MAVEN_COMPILER_PLUGIN_ARTIFACT_ID, null,
183+
"<configuration><annotationProcessorPaths/></configuration>", null, null, null
184+
).visit(tree, ctx);
185+
186+
// Then, configure the annotation processor path
187+
return new MavenIsoVisitor<ExecutionContext>() {
188+
@Override
189+
public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) {
190+
Xml.Tag plugins = super.visitTag(tag, ctx);
191+
plugins = (Xml.Tag) new MavenPlugin.Matcher(isParent, MAVEN_COMPILER_PLUGIN_GROUP_ID, MAVEN_COMPILER_PLUGIN_ARTIFACT_ID).asVisitor(plugin -> {
192+
MavenResolutionResult currentMrr = getResolutionResult();
193+
AtomicReference<TreeVisitor<?, ExecutionContext>> maybePropertyUpdate = new AtomicReference<>();
194+
195+
Xml.Tag modifiedPlugin = new XmlIsoVisitor<ExecutionContext>() {
196+
@Override
197+
public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) {
198+
Xml.Tag tg = super.visitTag(tag, ctx);
199+
200+
if (!"annotationProcessorPaths".equals(tg.getName())) {
201+
return tg;
202+
}
203+
204+
// Iterate the children (annotation processor paths) and try to update the version
79205
for (int i = 0; i < tg.getChildren().size(); i++) {
80206
Xml.Tag child = tg.getChildren().get(i);
81-
if (groupId.equals(child.getChildValue("groupId").orElse(null)) &&
82-
artifactId.equals(child.getChildValue("artifactId").orElse(null))) {
83-
if (!version.equals(child.getChildValue("version").orElse(null))) {
84-
String oldVersion = child.getChildValue("version").orElse("");
85-
boolean oldVersionUsesProperty = oldVersion.startsWith("${");
86-
String lookupVersion = oldVersionUsesProperty ?
87-
mrr.getPom().getValue(oldVersion.trim()) :
88-
oldVersion;
89-
VersionComparator comparator = Semver.validate(lookupVersion, null).getValue();
90-
if (comparator.compare(version, lookupVersion) > 0) {
91-
if (oldVersionUsesProperty) {
92-
afterVisitor.set(new ChangePropertyValue(oldVersion, version, null, null).getVisitor());
93-
} else {
94-
List<Xml.Tag> tags = tg.getChildren();
95-
tags.set(i, child.withChildValue("version", version));
96-
return tg.withContent(tags);
97-
}
207+
if (!groupId.equals(child.getChildValue("groupId").orElse(null)) ||
208+
!artifactId.equals(child.getChildValue("artifactId").orElse(null))) {
209+
continue;
210+
}
211+
212+
if (!version.equals(child.getChildValue("version").orElse(null))) {
213+
String oldVersion = child.getChildValue("version").orElse("");
214+
boolean oldVersionUsesProperty = oldVersion.startsWith("${");
215+
String lookupVersion = oldVersionUsesProperty ?
216+
currentMrr.getPom().getValue(oldVersion.trim()) : oldVersion;
217+
VersionComparator comparator = Semver.validate(lookupVersion, null).getValue();
218+
if (comparator.compare(version, lookupVersion) > 0) {
219+
if (oldVersionUsesProperty) {
220+
// A maven property is used here, update in properties section later
221+
maybePropertyUpdate.set(new ChangePropertyValue(oldVersion, version, null, null).getVisitor());
222+
} else {
223+
// Update the path's version directly
224+
List<Xml.Tag> tags = tg.getChildren();
225+
tags.set(i, child.withChildValue("version", version));
226+
return tg.withContent(tags);
98227
}
99228
}
100-
return tg;
101229
}
230+
231+
return tg;
102232
}
233+
234+
// Not found, so we add it
103235
return tg.withContent(ListUtils.concat(tg.getChildren(), Xml.Tag.build(String.format(
104236
"<path>\n<groupId>%s</groupId>\n<artifactId>%s</artifactId>\n<version>%s</version>\n</path>",
105237
groupId, artifactId, version))));
106238
}
107-
return tg;
239+
}.visitTag(plugin.getTree(), ctx);
240+
241+
if (maybePropertyUpdate.get() != null) {
242+
doAfterVisit(maybePropertyUpdate.get());
108243
}
109-
}.visitTag(plugin.getTree(), ctx);
110-
if (afterVisitor.get() != null) {
111-
doAfterVisit(afterVisitor.get());
244+
245+
return modifiedPlugin;
246+
}).visitNonNull(plugins, 0);
247+
248+
if (plugins != tag) {
249+
plugins = autoFormat(plugins, ctx);
112250
}
113-
return modifiedPlugin;
251+
return plugins;
114252
}
115-
return plugin.getTree();
116-
}).visitNonNull(plugins, 0);
117-
if (plugins != tag) {
118-
plugins = autoFormat(plugins, ctx);
119-
}
120-
return plugins;
253+
}.visit(tree, ctx);
121254
}
122255
};
123256
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.maven;
17+
18+
import lombok.EqualsAndHashCode;
19+
import lombok.Value;
20+
import org.intellij.lang.annotations.Language;
21+
import org.jspecify.annotations.Nullable;
22+
import org.openrewrite.ExecutionContext;
23+
import org.openrewrite.Option;
24+
import org.openrewrite.Recipe;
25+
import org.openrewrite.TreeVisitor;
26+
import org.openrewrite.xml.XPathMatcher;
27+
28+
@Value
29+
@EqualsAndHashCode(callSuper = false)
30+
public class AddManagedPlugin extends Recipe {
31+
@Option(displayName = "Group",
32+
description = "The first part of a dependency coordinate 'org.openrewrite.maven:rewrite-maven-plugin:VERSION'.",
33+
example = "org.openrewrite.maven")
34+
String groupId;
35+
36+
@Option(displayName = "Artifact",
37+
description = "The second part of a dependency coordinate 'org.openrewrite.maven:rewrite-maven-plugin:VERSION'.",
38+
example = "rewrite-maven-plugin")
39+
String artifactId;
40+
41+
@Option(displayName = "Version",
42+
description = "A fixed version of the plugin to add.",
43+
example = "1.0.0",
44+
required = false)
45+
@Nullable
46+
String version;
47+
48+
@Language("xml")
49+
@Option(displayName = "Configuration",
50+
description = "Optional plugin configuration provided as raw XML",
51+
example = "<configuration><foo>foo</foo></configuration>",
52+
required = false)
53+
@Nullable
54+
String configuration;
55+
56+
@Language("xml")
57+
@Option(displayName = "Dependencies",
58+
description = "Optional plugin dependencies provided as raw XML.",
59+
example = "<dependencies><dependency><groupId>com.yourorg</groupId><artifactId>core-lib</artifactId><version>1.0.0</version></dependency></dependencies>",
60+
required = false)
61+
@Nullable
62+
String dependencies;
63+
64+
@Language("xml")
65+
@Option(displayName = "Executions",
66+
description = "Optional executions provided as raw XML.",
67+
example = "<executions><execution><phase>generate-sources</phase><goals><goal>add-source</goal></goals></execution></executions>",
68+
required = false)
69+
@Nullable
70+
String executions;
71+
72+
@Option(displayName = "File pattern",
73+
description = "A glob expression that can be used to constrain which directories or source files should be searched. " +
74+
"Multiple patterns may be specified, separated by a semicolon `;`. " +
75+
"If multiple patterns are supplied any of the patterns matching will be interpreted as a match. " +
76+
"When not set, all source files are searched. ",
77+
required = false,
78+
example = "**/*-parent/grpc-*/pom.xml")
79+
@Nullable
80+
String filePattern;
81+
82+
String displayName = "Add Managed Maven plugin";
83+
84+
@Override
85+
public String getInstanceNameSuffix() {
86+
return String.format("`%s:%s:%s`", groupId, artifactId, version);
87+
}
88+
89+
String description = "Add the specified Maven plugin to the Plugin Managed of the pom.xml.";
90+
91+
@Override
92+
public TreeVisitor<?, ExecutionContext> getVisitor() {
93+
return new AddPluginVisitor(true, groupId, artifactId, version, configuration, dependencies, executions, filePattern);
94+
}
95+
}

0 commit comments

Comments
 (0)