Skip to content

Commit 455f682

Browse files
Jmockit Expectations - Handle Additional Cases (#418)
* JMockit to Mockito Recipe - handle more expectations block cases * Major refactoring - use visitMethodDeclaration and update cursor * slightly more targeted * prefix is already set * polish * comments * polish * comments --------- Co-authored-by: Tim te Beek <[email protected]>
1 parent 1ee57c8 commit 455f682

File tree

2 files changed

+486
-73
lines changed

2 files changed

+486
-73
lines changed

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

Lines changed: 158 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -15,79 +15,180 @@
1515
*/
1616
package org.openrewrite.java.testing.jmockit;
1717

18+
import java.util.ArrayList;
19+
import java.util.List;
20+
import java.util.Objects;
21+
import java.util.regex.Pattern;
22+
1823
import lombok.EqualsAndHashCode;
1924
import lombok.Value;
25+
import org.openrewrite.Cursor;
2026
import org.openrewrite.ExecutionContext;
2127
import org.openrewrite.Preconditions;
2228
import org.openrewrite.Recipe;
2329
import org.openrewrite.TreeVisitor;
30+
import org.openrewrite.java.JavaIsoVisitor;
2431
import org.openrewrite.java.JavaParser;
2532
import org.openrewrite.java.JavaTemplate;
26-
import org.openrewrite.java.JavaVisitor;
2733
import org.openrewrite.java.search.UsesType;
2834
import org.openrewrite.java.tree.Expression;
2935
import org.openrewrite.java.tree.J;
36+
import org.openrewrite.java.tree.JavaCoordinates;
37+
import org.openrewrite.java.tree.JavaType;
3038
import org.openrewrite.java.tree.Statement;
3139

3240
@Value
3341
@EqualsAndHashCode(callSuper = false)
3442
public class JMockitExpectationsToMockitoWhen extends Recipe {
35-
@Override
36-
public String getDisplayName() {
37-
return "Rewrite JMockit Expectations";
38-
}
39-
40-
@Override
41-
public String getDescription() {
42-
return "Rewrites JMockit `Expectations` to `Mockito.when`.";
43-
}
44-
45-
@Override
46-
public TreeVisitor<?, ExecutionContext> getVisitor() {
47-
return Preconditions.check(new UsesType<>("mockit.*", false),
48-
new RewriteExpectationsVisitor());
49-
}
50-
51-
private static class RewriteExpectationsVisitor extends JavaVisitor<ExecutionContext> {
5243
@Override
53-
public J visitNewClass(J.NewClass newClass, ExecutionContext executionContext) {
54-
J.NewClass nc = (J.NewClass) super.visitNewClass(newClass, executionContext);
55-
if (!(nc.getClazz() instanceof J.Identifier)) {
56-
return nc;
57-
}
58-
J.Identifier clazz = (J.Identifier) nc.getClazz();
59-
if (!clazz.getSimpleName().equals("Expectations")) {
60-
return nc;
61-
}
62-
63-
// empty Expectations block is considered invalid
64-
assert nc.getBody() != null : "Expectations block is empty";
65-
66-
// prepare the statements for moving
67-
J.Block innerBlock = (J.Block) nc.getBody().getStatements().get(0);
68-
69-
// TODO: handle multiple mock statements
70-
Statement mockInvocation = innerBlock.getStatements().get(0);
71-
Expression result = ((J.Assignment) innerBlock.getStatements().get(1)).getAssignment();
72-
73-
// apply the template and replace the `new Expectations()` statement coordinates
74-
// TODO: handle exception results with another template
75-
J.MethodInvocation newMethod = JavaTemplate.builder("when(#{any()}).thenReturn(#{});")
76-
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(executionContext, "mockito-core-3.12"))
77-
.staticImports("org.mockito.Mockito.when")
78-
.build()
79-
.apply(
80-
getCursor(),
81-
nc.getCoordinates().replace(),
82-
mockInvocation,
83-
result
84-
);
85-
86-
// handle import changes
87-
maybeAddImport("org.mockito.Mockito", "when");
88-
maybeRemoveImport("mockit.Expectations");
89-
90-
return newMethod.withPrefix(nc.getPrefix());
44+
public String getDisplayName() {
45+
return "Rewrite JMockit Expectations";
46+
}
47+
48+
@Override
49+
public String getDescription() {
50+
return "Rewrites JMockit `Expectations` to `Mockito.when`.";
51+
}
52+
53+
@Override
54+
public TreeVisitor<?, ExecutionContext> getVisitor() {
55+
return Preconditions.check(new UsesType<>("mockit.*", false),
56+
new RewriteExpectationsVisitor());
57+
}
58+
59+
private static class RewriteExpectationsVisitor extends JavaIsoVisitor<ExecutionContext> {
60+
61+
private static final String PRIMITIVE_RESULT_TEMPLATE = "when(#{any()}).thenReturn(#{});";
62+
private static final String OBJECT_RESULT_TEMPLATE = "when(#{any()}).thenReturn(#{any(java.lang.String)});";
63+
private static final String EXCEPTION_RESULT_TEMPLATE = "when(#{any()}).thenThrow(#{any()});";
64+
private static final Pattern EXPECTATIONS_PATTERN = Pattern.compile("mockit.Expectations");
65+
66+
// the LST element that is being updated when applying one of the java templates
67+
private Object cursorLocation;
68+
69+
// the coordinates where the next statement should be inserted
70+
private JavaCoordinates coordinates;
71+
72+
@Override
73+
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration methodDeclaration, ExecutionContext ctx) {
74+
J.MethodDeclaration md = super.visitMethodDeclaration(methodDeclaration, ctx);
75+
if (md.getBody() == null) {
76+
return md;
77+
}
78+
cursorLocation = md.getBody();
79+
J.Block newBody = md.getBody();
80+
List<Statement> statements = md.getBody().getStatements();
81+
82+
// iterate over each statement in the method body, find Expectations blocks and rewrite them
83+
for (int i = 0; i < statements.size(); i++) {
84+
Statement s = statements.get(i);
85+
if (!(s instanceof J.NewClass)) {
86+
continue;
87+
}
88+
J.NewClass nc = (J.NewClass) s;
89+
if (!(nc.getClazz() instanceof J.Identifier)) {
90+
continue;
91+
}
92+
J.Identifier clazz = (J.Identifier) nc.getClazz();
93+
if (clazz.getType() == null || !clazz.getType().isAssignableFrom(EXPECTATIONS_PATTERN)) {
94+
continue;
95+
}
96+
// empty Expectations block is considered invalid
97+
assert nc.getBody() != null && !nc.getBody().getStatements().isEmpty() : "Expectations block is empty";
98+
// Expectations block should be composed of a block within another block
99+
assert nc.getBody().getStatements().size() == 1 : "Expectations block is malformed";
100+
101+
// we have a valid Expectations block, update imports and rewrite with Mockito statements
102+
maybeAddImport("org.mockito.Mockito", "when");
103+
maybeRemoveImport("mockit.Expectations");
104+
105+
// the first coordinates are the coordinates the Expectations block, replacing it
106+
coordinates = nc.getCoordinates().replace();
107+
J.Block expectationsBlock = (J.Block) nc.getBody().getStatements().get(0);
108+
List<Statement> expectationStatements = expectationsBlock.getStatements();
109+
List<Object> templateParams = new ArrayList<>();
110+
111+
// iterate over the expectations statements and rebuild the method body
112+
for (Statement expectationStatement : expectationStatements) {
113+
// TODO: handle void methods (including final statement)
114+
115+
// TODO: handle additional jmockit expectations features
116+
117+
if (expectationStatement instanceof J.MethodInvocation) {
118+
if (!templateParams.isEmpty()) {
119+
// apply template to build new method body
120+
newBody = buildNewBody(ctx, templateParams, i);
121+
122+
// reset template params for next expectation
123+
templateParams = new ArrayList<>();
124+
}
125+
templateParams.add(expectationStatement);
126+
} else {
127+
// assignment
128+
templateParams.add(((J.Assignment) expectationStatement).getAssignment());
129+
}
130+
}
131+
132+
// handle the last statement
133+
if (!templateParams.isEmpty()) {
134+
newBody = buildNewBody(ctx, templateParams, i);
135+
}
136+
}
137+
138+
return md.withBody(newBody);
139+
}
140+
141+
private J.Block buildNewBody(ExecutionContext ctx, List<Object> templateParams, int newStatementIndex) {
142+
Expression result = (Expression) templateParams.get(1);
143+
String template = getTemplate(result);
144+
145+
J.Block newBody = JavaTemplate.builder(template)
146+
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "mockito-core-3.12"))
147+
.staticImports("org.mockito.Mockito.when")
148+
.build()
149+
.apply(
150+
new Cursor(getCursor(), cursorLocation),
151+
coordinates,
152+
templateParams.toArray()
153+
);
154+
155+
List<Statement> newStatements = new ArrayList<>(newBody.getStatements().size());
156+
for (int i = 0; i < newBody.getStatements().size(); i++) {
157+
Statement s = newBody.getStatements().get(i);
158+
if (i == newStatementIndex) {
159+
// next statement coordinates are immediately after the statement just added
160+
coordinates = s.getCoordinates().after();
161+
}
162+
newStatements.add(s);
163+
}
164+
newBody = newBody.withStatements(newStatements);
165+
166+
// cursor location is now the new body
167+
cursorLocation = newBody;
168+
169+
return newBody;
170+
}
171+
172+
/*
173+
* Based on the result type, we need to use a different template.
174+
*/
175+
private static String getTemplate(Expression result) {
176+
String template;
177+
JavaType resultType = Objects.requireNonNull(result.getType());
178+
if (resultType instanceof JavaType.Primitive) {
179+
template = PRIMITIVE_RESULT_TEMPLATE;
180+
} else if (resultType instanceof JavaType.Class) {
181+
Class<?> resultClass;
182+
try {
183+
resultClass = Class.forName(((JavaType.Class) resultType).getFullyQualifiedName());
184+
} catch (ClassNotFoundException e) {
185+
throw new RuntimeException(e);
186+
}
187+
template = Throwable.class.isAssignableFrom(resultClass) ? EXCEPTION_RESULT_TEMPLATE : OBJECT_RESULT_TEMPLATE;
188+
} else {
189+
throw new IllegalStateException("Unexpected value: " + result.getType());
190+
}
191+
return template;
192+
}
91193
}
92-
}
93194
}

0 commit comments

Comments
 (0)