Skip to content

Commit 5526dd1

Browse files
ryan-hudsonHudson, Ryantimtebeek
authored
Contribute AddMockitoJavaAgentToMavenSurefirePlugin Recipe (#1128)
* Contributing AddMockitoJavaAgentToMavenSurefirePlugin recipe for better recipe alignment to Mockito recommendations. * Contributing AddMockitoJavaAgentToMavenSurefirePlugin recipe for better recipe alignment to Mockito recommendations. * Remove stale AddSurefireFailsafeArgLineForMockito entry from recipes.csv * Simplify AddMockitoJavaAgentToMavenSurefirePlugin: guard matchers before per-tag work Move getArgLineJavaAgentArgument() and buildConfigurationTag() inside the matched surefire branches in visitTag so they no longer run for every tag in the pom, and replace the throwaway new ArrayList<>() default with emptyList(). * Use Lombok @Getter fields for recipe display name and description * Match surefire plugin via isPluginTag to cover pluginManagement Replace the /project/build/plugins XPath matchers in visitTag with the framework MavenVisitor.isPluginTag(groupId, artifactId), which matches any plugin under a plugins element (//plugins/plugin) and therefore also covers surefire declarations in build/pluginManagement/plugins. Handle the argLine configuration entirely at the plugin-tag level. Add a test augmenting a surefire plugin declared in pluginManagement. * Refactor AppendChildTagToParentVisitor to take prebuilt matcher and tag * Use child tag matching instead of print in AppendChildTagToParentVisitor * Strip xml declaration and project attributes from test poms * Minimize test poms by removing packaging and name elements * End test text blocks on their own line for consistency * Document why surefire config is left untouched with multiple argLine tags * Remove test for invalid multiple-argLine configuration --------- Co-authored-by: Hudson, Ryan <ryan.hudson@jpmchase.com> Co-authored-by: Tim te Beek <tim@moderne.io>
1 parent e3a46d5 commit 5526dd1

5 files changed

Lines changed: 1672 additions & 27 deletions

File tree

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/*
2+
* Copyright 2026 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (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://docs.moderne.io/licensing/moderne-source-available-license
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.java.migrate;
17+
18+
import lombok.Getter;
19+
import lombok.RequiredArgsConstructor;
20+
import org.intellij.lang.annotations.Language;
21+
import org.openrewrite.ExecutionContext;
22+
import org.openrewrite.Preconditions;
23+
import org.openrewrite.Recipe;
24+
import org.openrewrite.TreeVisitor;
25+
import org.openrewrite.internal.ListUtils;
26+
import org.openrewrite.maven.AddPlugin;
27+
import org.openrewrite.maven.AddPropertyVisitor;
28+
import org.openrewrite.maven.ChangePluginExecutions;
29+
import org.openrewrite.maven.MavenIsoVisitor;
30+
import org.openrewrite.maven.search.DependencyInsight;
31+
import org.openrewrite.maven.search.FindPlugin;
32+
import org.openrewrite.maven.tree.Plugin;
33+
import org.openrewrite.maven.tree.ResolvedDependency;
34+
import org.openrewrite.maven.tree.Scope;
35+
import org.openrewrite.semver.LatestRelease;
36+
import org.openrewrite.xml.AddOrUpdateChildTag;
37+
import org.openrewrite.xml.AddToTagVisitor;
38+
import org.openrewrite.xml.XPathMatcher;
39+
import org.openrewrite.xml.XmlIsoVisitor;
40+
import org.openrewrite.xml.tree.Content;
41+
import org.openrewrite.xml.tree.Xml;
42+
43+
import java.util.List;
44+
import java.util.Optional;
45+
46+
import static java.util.Collections.emptyList;
47+
48+
public class AddMockitoJavaAgentToMavenSurefirePlugin extends Recipe {
49+
50+
private static final String MAVEN_PLUGINS_GROUP_ID = "org.apache.maven.plugins";
51+
private static final String MAVEN_SUREFIRE_PLUGIN_ARTIFACT_ID = "maven-surefire-plugin";
52+
53+
@Language("xpath")
54+
private static final String MAVEN_DEPENDENCY_PLUGIN_EXECUTION_MATCHER = "/project/build/plugins/plugin[artifactId='maven-dependency-plugin']/executions/execution";
55+
56+
@Language("xml")
57+
private static final String MAVEN_DEPENDENCY_PLUGIN_PROPERTIES_GOAL = "<goal>properties</goal>";
58+
59+
@Language("xml")
60+
private static final String MAVEN_DEPENDENCY_PLUGIN_EXECUTION_TAG = "<execution><goals>"+ MAVEN_DEPENDENCY_PLUGIN_PROPERTIES_GOAL + "</goals></execution>";
61+
62+
@Getter
63+
final String displayName = "Add Mockito Java Agent to Maven Surefire Plugin";
64+
65+
@Getter
66+
final String description = "Adds required configuration to specifically enable the Mockito/Bytebuddy Java agent in the Maven Surefire plugin for Java 21 compatibility.";
67+
68+
@Override
69+
public TreeVisitor<?, ExecutionContext> getVisitor() {
70+
return Preconditions.check(new DependencyInsight("org.mockito", "mockito-core", "test", null, false), new MavenIsoVisitor<ExecutionContext>() {
71+
private final String CONFIGURATION_TAG_TEMPLATE = "<configuration><!--suppress MavenModelInspection --><argLine>%s</argLine></configuration>";
72+
73+
private String getArgLineJavaAgentArgument() {
74+
String mockitoCoreVersion = getResolutionResult().getDependencies().getOrDefault(Scope.Test, emptyList()).stream()
75+
.filter(dependency -> dependency.getGroupId().equals("org.mockito") && dependency.getArtifactId()
76+
.equals("mockito-core")).findFirst().map(ResolvedDependency::getVersion).get();
77+
78+
return new LatestRelease(null).compare(null, mockitoCoreVersion, "5.14.0") >= 0 ?
79+
"-javaagent:${org.mockito:mockito-core:jar}" : "-javaagent:${net.bytebuddy:byte-buddy-agent:jar}";
80+
}
81+
82+
private Xml.Tag buildConfigurationTag(String argLineJavaAgentParam, boolean hasExistingArgLine) {
83+
return Xml.Tag.build(String.format(CONFIGURATION_TAG_TEMPLATE, hasExistingArgLine ? argLineJavaAgentParam : "@{argLine} " + argLineJavaAgentParam));
84+
}
85+
86+
private void maybeAddMavenDependencyPluginWithPropertiesGoal() {
87+
Optional<Plugin> mavenDependencyPlugin = getResolutionResult().getPom().getPlugins().stream()
88+
.filter(plugin -> plugin.getGroupId().equals("org.apache.maven.plugins")
89+
&& plugin.getArtifactId().equals("maven-dependency-plugin")).findFirst();
90+
91+
if (!mavenDependencyPlugin.isPresent()) {
92+
doAfterVisit(new AddPlugin("org.apache.maven.plugins", "maven-dependency-plugin", null, null, null,
93+
"<executions>" + MAVEN_DEPENDENCY_PLUGIN_EXECUTION_TAG + "</executions>", "**/pom.xml").getVisitor());
94+
} else if (mavenDependencyPlugin.get().getExecutions().isEmpty()) {
95+
doAfterVisit(new ChangePluginExecutions("org.apache.maven.plugins", "maven-dependency-plugin", MAVEN_DEPENDENCY_PLUGIN_EXECUTION_TAG).getVisitor());
96+
} else if (mavenDependencyPlugin.get().getExecutions().stream().noneMatch(execution -> execution.getGoals() != null)) {
97+
doAfterVisit(new AddOrUpdateChildTag(MAVEN_DEPENDENCY_PLUGIN_EXECUTION_MATCHER, "<goals>" + MAVEN_DEPENDENCY_PLUGIN_PROPERTIES_GOAL + "</goals>", false).getVisitor());
98+
} else if (mavenDependencyPlugin.get().getExecutions().stream().noneMatch(execution -> execution.getGoals() != null && execution.getGoals().contains("properties"))) {
99+
doAfterVisit(new AppendChildTagToParentVisitor(
100+
new XPathMatcher(MAVEN_DEPENDENCY_PLUGIN_EXECUTION_MATCHER + "/goals"),
101+
Xml.Tag.build(MAVEN_DEPENDENCY_PLUGIN_PROPERTIES_GOAL)));
102+
}
103+
}
104+
105+
@Override
106+
public Xml.Document visitDocument(Xml.Document document, ExecutionContext ctx) {
107+
if (getResolutionResult().getPom().getPluginManagement().stream().anyMatch(
108+
plugin -> plugin.getGroupId().equals("org.apache.maven.plugins") && plugin.getArtifactId()
109+
.equals("maven-surefire-plugin") && plugin.getConfigurationStringValue("argLine") != null && plugin.getConfigurationStringValue("argLine").contains(getArgLineJavaAgentArgument()))) {
110+
return document;
111+
}
112+
113+
maybeAddMavenDependencyPluginWithPropertiesGoal();
114+
doAfterVisit(new AddPropertyVisitor("argLine", "", true));
115+
116+
if (FindPlugin.find(document, "org.apache.maven.plugins", "maven-surefire-plugin").isEmpty()) {
117+
doAfterVisit(new AddPlugin("org.apache.maven.plugins", "maven-surefire-plugin", null,
118+
String.format(CONFIGURATION_TAG_TEMPLATE, "@{argLine} " + getArgLineJavaAgentArgument()), null,
119+
null, null).getVisitor());
120+
return document;
121+
}
122+
return super.visitDocument(document, ctx);
123+
}
124+
125+
@Override
126+
public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) {
127+
Xml.Tag t = super.visitTag(tag, ctx);
128+
// `isPluginTag` matches any `plugin` under a `plugins` element, so this covers both
129+
// `build/plugins` and `build/pluginManagement/plugins` declarations.
130+
if (!isPluginTag(MAVEN_PLUGINS_GROUP_ID, MAVEN_SUREFIRE_PLUGIN_ARTIFACT_ID)) {
131+
return t;
132+
}
133+
134+
String argLineJavaAgentParam = getArgLineJavaAgentArgument();
135+
//noinspection unchecked
136+
List<Content> pluginContents = (List<Content>) t.getContent();
137+
Optional<Xml.Tag> configuration = t.getChild("configuration");
138+
if (!configuration.isPresent()) {
139+
return autoFormat(t.withContent(ListUtils.concat(pluginContents,
140+
buildConfigurationTag(argLineJavaAgentParam, false))), ctx);
141+
}
142+
143+
Xml.Tag config = configuration.get();
144+
//noinspection unchecked
145+
List<Content> configContents = (List<Content>) config.getContent();
146+
List<Xml.Tag> argLineTagChildren = config.getChildren("argLine");
147+
if (argLineTagChildren.isEmpty()) {
148+
Xml.Tag updatedConfig = config.withContent(ListUtils.concatAll(configContents,
149+
buildConfigurationTag(argLineJavaAgentParam, false).getContent()));
150+
return autoFormat(t.withContent(ListUtils.map(pluginContents, c -> c == config ? updatedConfig : c)), ctx);
151+
}
152+
if (argLineTagChildren.size() == 1) {
153+
Xml.Tag argLineTag = argLineTagChildren.get(0);
154+
String existingArgLineValue = argLineTag.getValue().orElse("@{argLine}");
155+
156+
if (!existingArgLineValue.contains(argLineJavaAgentParam)) {
157+
List<Content> nonArgLineTags = ListUtils.filter(configContents, content -> content != argLineTag);
158+
Xml.Tag mergedConfiguration = buildConfigurationTag(existingArgLineValue + " " + argLineJavaAgentParam, true);
159+
Xml.Tag updatedConfig = config.withContent(ListUtils.concatAll(nonArgLineTags, mergedConfiguration.getContent()));
160+
return autoFormat(t.withContent(ListUtils.map(pluginContents, c -> c == config ? updatedConfig : c)), ctx);
161+
}
162+
}
163+
// No surefire change needed: the single argLine already contains the agent, or
164+
// there are multiple <argLine> tags (an invalid configuration, since surefire's
165+
// argLine is single-valued) that we leave untouched.
166+
return t;
167+
}
168+
});
169+
}
170+
171+
@RequiredArgsConstructor
172+
private static class AppendChildTagToParentVisitor extends XmlIsoVisitor<ExecutionContext> {
173+
private final XPathMatcher parentXPathMatcher;
174+
private final Xml.Tag newChildTag;
175+
176+
@Override
177+
public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) {
178+
if (parentXPathMatcher.matches(getCursor()) &&
179+
tag.getChildren(newChildTag.getName()).stream().noneMatch(child -> child.getValue().equals(newChildTag.getValue()))) {
180+
return autoFormat(AddToTagVisitor.addToTag(tag, newChildTag, getCursor()), ctx);
181+
}
182+
return super.visitTag(tag, ctx);
183+
}
184+
}
185+
}

src/main/resources/META-INF/rewrite/java-version-25.yml

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -221,27 +221,7 @@ recipeList:
221221
groupId: org.mockito
222222
artifactId: mockito-*
223223
newVersion: 5.17.x
224-
- org.openrewrite.java.migrate.AddSurefireFailsafeArgLineForMockito
225-
226-
---
227-
type: specs.openrewrite.org/v1beta/recipe
228-
name: org.openrewrite.java.migrate.AddSurefireFailsafeArgLineForMockito
229-
displayName: Add surefire `--add-opens` for Mockito/ByteBuddy
230-
description: >-
231-
Adds `--add-opens` JVM arguments required by Mockito and ByteBuddy to the Maven Surefire and Failsafe plugin
232-
`argLine` configuration. Only applied when the project depends on Mockito.
233-
preconditions:
234-
- org.openrewrite.java.dependencies.search.ModuleHasDependency:
235-
groupIdPattern: org.mockito
236-
artifactIdPattern: mockito-*
237-
recipeList:
238-
- org.openrewrite.java.migrate.AddSurefireFailsafeArgLine:
239-
argLine: >-
240-
--add-opens java.base/java.lang=ALL-UNNAMED
241-
--add-opens java.base/java.lang.reflect=ALL-UNNAMED
242-
--add-opens java.base/jdk.internal.misc=ALL-UNNAMED
243-
--add-opens java.base/jdk.internal.reflect=ALL-UNNAMED
244-
-Dnet.bytebuddy.experimental=true
224+
- org.openrewrite.java.migrate.AddMockitoJavaAgentToMavenSurefirePlugin
245225

246226
---
247227
type: specs.openrewrite.org/v1beta/recipe

0 commit comments

Comments
 (0)