Skip to content

Commit 05d0013

Browse files
Various JMockit fixes (#824)
* Fix mockStatic error in JMockitToMockito (#776) - Use getFullyQualifiedName() instead of getClassName() for nullable() matchers to avoid ambiguity with same-named classes - Fix syntax error in getCallRealMethod() by properly handling semicolon - Add ShortenFullyQualifiedTypeReferences to shorten FQN types where safe - Add test case to verify thenCallRealMethod() generates correct syntax 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * Fix variable name conflicts in JMockitToMockito (#777) When setup statements are moved out of Expectations blocks, variable declarations in the setup statements could conflict with variable declarations in the method body, causing compilation errors. This fix detects such conflicts and wraps the setup statements in a block to maintain proper scoping when conflicts are detected. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * Fix IndexOutOfBoundsException in RemoveTimesZeroAndOne with custom classes Fixes #748 When converting JMockit Verifications with `times = 1` to Mockito for custom classes, the RemoveTimesZeroAndOne recipe was failing with IndexOutOfBoundsException because getParameterTypes() returned an empty list when type information was incomplete. Changes: - Check if parameter types list is empty before calling subList() - If empty, only remove the times() argument without modifying method type - Add test case with custom class to verify the fix - Disable method invocation type validation in the test as type info may be incomplete * Refactor to use direct AST manipulation instead of JavaTemplate Previously, `removeBlock()` and `rewriteBodyStatement()` were using JavaTemplate inappropriately - using empty strings or `#{any()}` just to manipulate AST structure. This was inefficient and error-prone. Changes: - Refactored `removeBlock()` to directly manipulate the statements list - Refactored `rewriteBodyStatement()` to use direct AST manipulation with proper tree node comparison using `.isScope()` - Fixed issue #687: Added check for empty Expectations blocks to prevent IndexOutOfBoundsException - Added regression test `emptyExpectationsBlockDoesNotCrash()` * Add contextSensitive() to JavaTemplate in JMockitBlockRewriter Using `.contextSensitive()` allows the JavaTemplate to use type information from the surrounding code context, which can improve type attribution in generated code. While this doesn't fully solve issue #748's type attribution problem with custom classes (which still requires disabling type validation for method invocations), it's a best practice that may help in other scenarios. All 91 tests pass with 100% success. * Fix type attribution in verify() calls by using AST manipulation Issue #748: verify() calls with custom classes were generating "MethodInvocation type is missing or malformed" errors. Root cause: JavaTemplate can only resolve types on the classpath, not types defined in the source code being transformed. Solution: Template verify() with placeholder types (String, int), then replace placeholders with actual AST nodes from the original code: 1. Template: verify("", times(1)).toString() 2. Replace "" with actual mock 3. Replace 1 with actual times value 4. Replace toString() with actual method invocation This preserves all type information from the original invocation. Also fixed: Filter out times(1) since it's the default. Fixes #748 All 91 JMockit tests pass with full type validation enabled. * Polish * Polish * Polish --------- Co-authored-by: Claude <[email protected]>
1 parent 093bc47 commit 05d0013

File tree

9 files changed

+410
-53
lines changed

9 files changed

+410
-53
lines changed

src/main/java/org/openrewrite/java/testing/jmockit/JMockitBlockRewriter.java

Lines changed: 103 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import org.jspecify.annotations.Nullable;
2222
import org.openrewrite.Cursor;
2323
import org.openrewrite.ExecutionContext;
24-
import org.openrewrite.java.JavaParser;
2524
import org.openrewrite.java.JavaTemplate;
2625
import org.openrewrite.java.JavaVisitor;
2726
import org.openrewrite.java.tree.*;
@@ -174,7 +173,12 @@ private void rewriteMethodInvocation(List<Statement> statementsToRewrite) {
174173
return;
175174
}
176175
if (mockInvocationResults.getTimes() != null) {
177-
rewriteVerify(invocation, mockInvocationResults.getTimes(), "times");
176+
// Don't add times(1) since that's the default
177+
if (!J.Literal.isLiteralValue(mockInvocationResults.getTimes(), 1)) {
178+
rewriteVerify(invocation, mockInvocationResults.getTimes(), "times");
179+
} else {
180+
rewriteVerify(invocation, null, "");
181+
}
178182
}
179183
if (mockInvocationResults.getMinTimes() != null) {
180184
rewriteVerify(invocation, mockInvocationResults.getMinTimes(), "atLeast");
@@ -185,10 +189,11 @@ private void rewriteMethodInvocation(List<Statement> statementsToRewrite) {
185189
}
186190

187191
private void removeBlock() {
188-
methodBody = JavaTemplate.builder("")
189-
.javaParser(JavaParser.fromJavaVersion())
190-
.build()
191-
.apply(new Cursor(visitor.getCursor(), methodBody), nextStatementCoordinates);
192+
List<Statement> statements = new ArrayList<>(methodBody.getStatements());
193+
if (bodyStatementIndex >= 0 && bodyStatementIndex < statements.size()) {
194+
statements.remove(bodyStatementIndex);
195+
methodBody = methodBody.withStatements(statements);
196+
}
192197
setNextStatementCoordinates(0);
193198
}
194199

@@ -224,32 +229,108 @@ private void rewriteVerify(J.MethodInvocation invocation, @Nullable Expression t
224229
return;
225230
}
226231

227-
List<Object> templateParams = new ArrayList<>();
228-
templateParams.add(invocation.getSelect());
229-
if (times != null) {
230-
templateParams.add(times);
232+
// Build a template for verify() using placeholder types to avoid classpath dependencies
233+
// We'll template something like: verify("", times(1)).toString();
234+
// Then replace "" with the mock, 1 with the actual times, and toString() with the actual method invocation
235+
StringBuilder templateBuilder = new StringBuilder();
236+
237+
if (isVerificationsInOrder()) {
238+
templateBuilder.append("inOrder");
239+
if (this.verificationsInOrderIdx > 0) {
240+
templateBuilder.append(this.verificationsInOrderIdx);
241+
}
242+
templateBuilder.append(".");
231243
}
232-
templateParams.add(invocation.getName().getSimpleName());
233-
String verifyTemplate = getVerifyTemplate(invocation.getArguments(), verificationMode, templateParams);
244+
245+
templateBuilder.append("verify(\"\"");
246+
if (!verificationMode.isEmpty()) {
247+
templateBuilder.append(", ").append(verificationMode).append("(1)");
248+
}
249+
templateBuilder.append(").toString();");
250+
234251
JavaCoordinates verifyCoordinates;
235252
if (this.blockType.isVerifications()) {
236-
// for Verifications, replace the Verifications block
237253
verifyCoordinates = nextStatementCoordinates;
238254
} else {
239-
// for Expectations put verify at the end of the method
240255
verifyCoordinates = methodBody.getCoordinates().lastStatement();
241256
}
242-
rewriteTemplate(verifyTemplate, templateParams, verifyCoordinates);
243-
if (this.rewriteFailed) {
257+
258+
// Apply template to add the verify wrapper
259+
int numStatementsBefore = methodBody.getStatements().size();
260+
methodBody = JavaTemplate.builder(templateBuilder.toString())
261+
.javaParser(getJavaParser(ctx))
262+
.staticImports(MOCKITO_ALL_IMPORT)
263+
.imports(IN_ORDER_IMPORT_FQN)
264+
.build()
265+
.apply(new Cursor(visitor.getCursor(), methodBody), verifyCoordinates);
266+
267+
if (methodBody.getStatements().size() <= numStatementsBefore) {
268+
this.rewriteFailed = true;
269+
return;
270+
}
271+
272+
// Find the newly added statement
273+
int newStatementIdx = this.blockType.isVerifications() ?
274+
bodyStatementIndex + numStatementsAdded :
275+
methodBody.getStatements().size() - 1;
276+
277+
Statement newStatement = methodBody.getStatements().get(newStatementIdx);
278+
if (!(newStatement instanceof J.MethodInvocation)) {
279+
this.rewriteFailed = true;
280+
return;
281+
}
282+
283+
// newStatement is something like: verify("").toString() or verify("", times(1)).toString()
284+
// We need to replace "" with the actual mock, 1 with the actual times, and toString() with the actual method invocation
285+
J.MethodInvocation toStringCall = (J.MethodInvocation) newStatement;
286+
287+
// Get the verify() call
288+
Expression selectExpr = toStringCall.getSelect();
289+
if (!(selectExpr instanceof J.MethodInvocation)) {
290+
this.rewriteFailed = true;
291+
return;
292+
}
293+
294+
J.MethodInvocation verifyCall = (J.MethodInvocation) selectExpr;
295+
296+
// Replace the placeholders in verify()'s arguments
297+
// First argument is always the mock ("")
298+
// Second argument (if present) is times(1)
299+
List<Expression> verifyArgs = new ArrayList<>(verifyCall.getArguments());
300+
if (verifyArgs.isEmpty()) {
301+
this.rewriteFailed = true;
244302
return;
245303
}
246304

305+
// Replace the mock placeholder
306+
verifyArgs.set(0, invocation.getSelect());
307+
308+
// If there's a second argument, it's the times() call - replace its argument
309+
if (verifyArgs.size() > 1 && verifyArgs.get(1) instanceof J.MethodInvocation) {
310+
J.MethodInvocation timesCall = (J.MethodInvocation) verifyArgs.get(1);
311+
if (times != null) {
312+
// Replace the placeholder argument with the actual times value
313+
List<Expression> timesArgs = new ArrayList<>();
314+
timesArgs.add(times);
315+
timesCall = timesCall.withArguments(timesArgs);
316+
verifyArgs.set(1, timesCall);
317+
}
318+
}
319+
320+
verifyCall = verifyCall.withArguments(verifyArgs);
321+
322+
// Replace toString() with the actual method invocation
323+
J.MethodInvocation wrappedInvocation = invocation.withSelect(verifyCall);
324+
325+
// Update the statement in the method body
326+
List<Statement> statements = new ArrayList<>(methodBody.getStatements());
327+
statements.set(newStatementIdx, wrappedInvocation);
328+
methodBody = methodBody.withStatements(statements);
329+
247330
if (this.blockType.isVerifications()) {
248-
setNextStatementCoordinates(++numStatementsAdded); // for Expectations, verify statements added to end of method
331+
setNextStatementCoordinates(++numStatementsAdded);
249332
}
250333

251-
// do this last making sure rewrite worked and specify onlyifReferenced=false because framework cannot find the
252-
// static reference to verify when another static mockit reference is added
253334
visitor.maybeAddImport(MOCKITO_IMPORT_FQN_PREFX, "verify", false);
254335
if (!verificationMode.isEmpty()) {
255336
visitor.maybeAddImport(MOCKITO_IMPORT_FQN_PREFX, verificationMode);
@@ -487,9 +568,9 @@ private String getVerifyTemplate(List<Expression> arguments, String
487568
private static class MockInvocationResults {
488569
@Setter(AccessLevel.NONE)
489570
private final List<Expression> results = new ArrayList<>();
490-
private Expression times;
491-
private Expression minTimes;
492-
private Expression maxTimes;
571+
private @Nullable Expression times;
572+
private @Nullable Expression minTimes;
573+
private @Nullable Expression maxTimes;
493574

494575
private void addResult(Expression result) {
495576
results.add(result);

src/main/java/org/openrewrite/java/testing/jmockit/JMockitBlockToMockito.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration methodDecl
9393
bodyStatementIndex++;
9494
}
9595
}
96-
return md.withBody(methodBody);
96+
return maybeAutoFormat(methodDeclaration, md.withBody(methodBody), ctx);
9797
}
9898
}
9999
}

src/main/java/org/openrewrite/java/testing/jmockit/JMockitMockUpToMockito.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.openrewrite.*;
1919
import org.openrewrite.java.JavaIsoVisitor;
2020
import org.openrewrite.java.JavaTemplate;
21+
import org.openrewrite.java.ShortenFullyQualifiedTypeReferences;
2122
import org.openrewrite.java.search.UsesType;
2223
import org.openrewrite.java.tree.*;
2324
import org.openrewrite.marker.SearchResult;
@@ -157,9 +158,7 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration methodDecl
157158
}
158159
if (isTearDownMethod(md)) {
159160
for (J.Identifier id : tearDownMocks.values()) {
160-
String type = TypeUtils.asFullyQualified(id.getFieldType().getType()).getFullyQualifiedName();
161-
md = JavaTemplate.builder("#{any(" + type + ")}.closeOnDemand();")
162-
.contextSensitive()
161+
md = JavaTemplate.builder("#{any()}.closeOnDemand();")
163162
.javaParser(getJavaParser(ctx))
164163
.imports(MOCKITO_STATIC_IMPORT, MOCKITO_CONSTRUCTION_IMPORT)
165164
.staticImports(MOCKITO_ALL_IMPORT)
@@ -307,6 +306,7 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration methodDecl
307306
maybeRemoveImport(JMOCKIT_MOCKUP_IMPORT);
308307

309308
doAfterVisit(new LambdaBlockToExpression().getVisitor());
309+
doAfterVisit(ShortenFullyQualifiedTypeReferences.modifyOnly(md));
310310
return maybeAutoFormat(methodDecl, md, ctx);
311311
}
312312

@@ -335,7 +335,7 @@ private String getMatcher(JavaType s) {
335335
String elem = TypeUtils.asArray(s).getElemType().toString();
336336
return "nullable(" + elem + "[].class)";
337337
}
338-
return "nullable(" + TypeUtils.asFullyQualified(s).getClassName() + ".class)";
338+
return "nullable(" + TypeUtils.asFullyQualified(s).getFullyQualifiedName() + ".class)";
339339
}
340340

341341
private String getAnswerBody(J.MethodDeclaration md) {
@@ -435,7 +435,6 @@ private String getMockStaticMethods(JavaType.Class clazz, String className, Map<
435435
.forEach(m -> tpl.append("mockStatic").append(className).append(".when(() -> ")
436436
.append(className).append(".").append(m.getName())
437437
.append(getCallRealMethod(m))
438-
.append(");")
439438
);
440439

441440
return tpl.toString();

src/main/java/org/openrewrite/java/testing/jmockit/SetupStatementsRewriter.java

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@
1515
*/
1616
package org.openrewrite.java.testing.jmockit;
1717

18-
import org.openrewrite.Cursor;
1918
import org.openrewrite.ExecutionContext;
20-
import org.openrewrite.java.JavaParser;
21-
import org.openrewrite.java.JavaTemplate;
2219
import org.openrewrite.java.JavaVisitor;
2320
import org.openrewrite.java.tree.*;
2421

@@ -55,6 +52,12 @@ J.Block rewriteMethodBody() {
5552
}
5653

5754
assert nc.getBody() != null;
55+
56+
// Check if the body has any statements - if empty, skip processing
57+
if (nc.getBody().getStatements().isEmpty()) {
58+
continue;
59+
}
60+
5861
J.Block expectationsBlock = (J.Block) nc.getBody().getStatements().get(0);
5962

6063
// Account for Expectations which may contain multiple blocks
@@ -66,18 +69,37 @@ J.Block rewriteMethodBody() {
6669
statementList.add(expectationsBlock);
6770
}
6871

69-
// statement needs to be moved directly before expectations class instantiation
70-
JavaCoordinates coordinates = nc.getCoordinates().before();
72+
// collect setup statements
73+
List<Statement> setupStatements = new ArrayList<>();
7174
List<Statement> newExpectationsBlockStatements = new ArrayList<>();
7275
for (Statement st : statementList) {
7376
for (Statement expectationStatement : ((J.Block) st).getStatements()) {
7477
if (!isSetupStatement(expectationStatement, spies)) {
7578
newExpectationsBlockStatements.add(expectationStatement);
7679
continue;
7780
}
78-
rewriteBodyStatement(expectationStatement, coordinates);
79-
// subsequent setup statements are moved in order
80-
coordinates = expectationStatement.getCoordinates().after();
81+
setupStatements.add(expectationStatement);
82+
}
83+
}
84+
85+
// check if setup statement variable names conflict with method body variable names
86+
Set<String> setupVariableNames = getVariableNames(setupStatements);
87+
Set<String> methodBodyVariableNames = getVariableNames(methodBody.getStatements());
88+
boolean hasConflict = setupVariableNames.stream().anyMatch(methodBodyVariableNames::contains);
89+
90+
// move setup statements before the expectations block
91+
JavaCoordinates coordinates = nc.getCoordinates().before();
92+
if (!setupStatements.isEmpty()) {
93+
if (hasConflict) {
94+
// wrap in a block to avoid variable name conflicts
95+
J.Block setupBlock = expectationsBlock.withStatements(setupStatements);
96+
rewriteBodyStatement(setupBlock, coordinates);
97+
} else {
98+
// move statements individually
99+
for (Statement setupStatement : setupStatements) {
100+
rewriteBodyStatement(setupStatement, coordinates);
101+
coordinates = setupStatement.getCoordinates().after();
102+
}
81103
}
82104
}
83105

@@ -91,14 +113,31 @@ J.Block rewriteMethodBody() {
91113
}
92114

93115
private void rewriteBodyStatement(Statement statement, JavaCoordinates coordinates) {
94-
methodBody = JavaTemplate.builder("#{any()}")
95-
.javaParser(JavaParser.fromJavaVersion())
96-
.build()
97-
.apply(
98-
new Cursor(visitor.getCursor(), methodBody),
99-
coordinates,
100-
statement
101-
);
116+
List<Statement> statements = new ArrayList<>(methodBody.getStatements());
117+
118+
if (coordinates.getMode() == JavaCoordinates.Mode.REPLACEMENT) {
119+
// Replace the statement at the specified position
120+
for (int i = 0; i < statements.size(); i++) {
121+
if (statements.get(i).isScope(coordinates.getTree())) {
122+
statements.set(i, statement);
123+
break;
124+
}
125+
}
126+
} else {
127+
// Find the reference statement and insert before/after it
128+
for (int i = 0; i < statements.size(); i++) {
129+
if (statements.get(i).isScope(coordinates.getTree())) {
130+
if (coordinates.getMode() == JavaCoordinates.Mode.BEFORE) {
131+
statements.add(i, statement);
132+
} else { // AFTER
133+
statements.add(i + 1, statement);
134+
}
135+
break;
136+
}
137+
}
138+
}
139+
140+
methodBody = methodBody.withStatements(statements);
102141
}
103142

104143
private boolean isSetupStatement(Statement expectationStatement, Set<String> spies) {
@@ -163,4 +202,17 @@ private static boolean isNotMockIdentifier(J.Identifier identifier, Set<String>
163202
}
164203
return true;
165204
}
205+
206+
private static Set<String> getVariableNames(List<Statement> statements) {
207+
Set<String> variableNames = new HashSet<>();
208+
for (Statement statement : statements) {
209+
if (statement instanceof J.VariableDeclarations) {
210+
J.VariableDeclarations varDecls = (J.VariableDeclarations) statement;
211+
for (J.VariableDeclarations.NamedVariable namedVar : varDecls.getVariables()) {
212+
variableNames.add(namedVar.getSimpleName());
213+
}
214+
}
215+
}
216+
return variableNames;
217+
}
166218
}

src/main/java/org/openrewrite/java/testing/mockito/RemoveTimesZeroAndOne.java

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,20 @@ public TreeVisitor<?, ExecutionContext> getVisitor() {
6666
J.MethodInvocation times = (J.MethodInvocation) mi.getArguments().get(1);
6767
if (timesMatcher.matches(times) && J.Literal.isLiteralValue(times.getArguments().get(0), 1)) {
6868
maybeRemoveImport("org.mockito.Mockito.times");
69-
JavaType.Method methodType = mi.getMethodType()
70-
.withParameterNames(mi.getMethodType().getParameterNames().subList(0, 1))
71-
.withParameterTypes(mi.getMethodType().getParameterTypes().subList(0, 1));
72-
return mi
73-
.withArguments(mi.getArguments().subList(0, 1))
74-
.withMethodType(methodType)
75-
.withName(mi.getName().withType(methodType));
69+
// Only update method type if parameter types are available
70+
JavaType.Method methodType = mi.getMethodType();
71+
if (!methodType.getParameterTypes().isEmpty()) {
72+
methodType = methodType
73+
.withParameterNames(methodType.getParameterNames().subList(0, 1))
74+
.withParameterTypes(methodType.getParameterTypes().subList(0, 1));
75+
return mi
76+
.withArguments(mi.getArguments().subList(0, 1))
77+
.withMethodType(methodType)
78+
.withName(mi.getName().withType(methodType));
79+
}
80+
// If parameter types are not available, just remove the second argument
81+
// and let the type attribution be redone later
82+
return mi.withArguments(mi.getArguments().subList(0, 1));
7683
}
7784
}
7885
return mi;

0 commit comments

Comments
 (0)