Skip to content

Commit e9f108c

Browse files
JMockit to Mockito Recipe - Improved Expectations Handling (#455)
* JMockit to Mockito Recipe - handle times field in Expectations blocks * Handle mock method arguments * parameterized result types for templates * whitespace * Refactor argument matchers rewriting into separate class * More refactoring * polish * handle multiple setup statements properly * openrewrite will format as best it can * handle chained method invocations * JMockit to Mockito Recipe - handle mock select fields, methods without results can be dropped, prep for better argument matchers handling * remove debug logs * one more debug log * Handle methods with mixed arguments and argument matchers * Handle more argument matcher cases and more annotations * Handle null params * Handle indexes properly * Handle spies and polish * polish * Handle returns statements * polish * remove logging * polish * polish * polish * polish * run formatter * polish * prefer anyString() over any(java.lang.String.class) * more polish * polish * Add license headers * polish * polish * whitespace * use final in non-obvious cases * consolidate methods * polish * Rename test to match recipe; strip common prefix --------- Co-authored-by: Tim te Beek <[email protected]>
1 parent 430f635 commit e9f108c

File tree

7 files changed

+1107
-261
lines changed

7 files changed

+1107
-261
lines changed
Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
/*
2+
* Copyright 2024 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.java.testing.jmockit;
17+
18+
import org.openrewrite.Cursor;
19+
import org.openrewrite.ExecutionContext;
20+
import org.openrewrite.java.JavaParser;
21+
import org.openrewrite.java.JavaTemplate;
22+
import org.openrewrite.java.JavaVisitor;
23+
import org.openrewrite.java.tree.Expression;
24+
import org.openrewrite.java.tree.J;
25+
import org.openrewrite.java.tree.JavaType;
26+
import org.openrewrite.java.tree.Statement;
27+
28+
import java.util.*;
29+
30+
class ArgumentMatchersRewriter {
31+
32+
private static final Set<String> JMOCKIT_ARGUMENT_MATCHERS = new HashSet<>();
33+
34+
static {
35+
JMOCKIT_ARGUMENT_MATCHERS.add("anyString");
36+
JMOCKIT_ARGUMENT_MATCHERS.add("anyInt");
37+
JMOCKIT_ARGUMENT_MATCHERS.add("anyLong");
38+
JMOCKIT_ARGUMENT_MATCHERS.add("anyDouble");
39+
JMOCKIT_ARGUMENT_MATCHERS.add("anyFloat");
40+
JMOCKIT_ARGUMENT_MATCHERS.add("anyBoolean");
41+
JMOCKIT_ARGUMENT_MATCHERS.add("anyByte");
42+
JMOCKIT_ARGUMENT_MATCHERS.add("anyChar");
43+
JMOCKIT_ARGUMENT_MATCHERS.add("anyShort");
44+
JMOCKIT_ARGUMENT_MATCHERS.add("any");
45+
}
46+
47+
private static final Map<String, String> MOCKITO_COLLECTION_MATCHERS = new HashMap<>();
48+
49+
static {
50+
MOCKITO_COLLECTION_MATCHERS.put("java.util.List", "anyList");
51+
MOCKITO_COLLECTION_MATCHERS.put("java.util.Set", "anySet");
52+
MOCKITO_COLLECTION_MATCHERS.put("java.util.Collection", "anyCollection");
53+
MOCKITO_COLLECTION_MATCHERS.put("java.util.Iterable", "anyIterable");
54+
MOCKITO_COLLECTION_MATCHERS.put("java.util.Map", "anyMap");
55+
}
56+
57+
private final JavaVisitor<ExecutionContext> visitor;
58+
private final ExecutionContext ctx;
59+
private final J.Block expectationsBlock;
60+
61+
ArgumentMatchersRewriter(JavaVisitor<ExecutionContext> visitor, ExecutionContext ctx, J.Block expectationsBlock) {
62+
this.visitor = visitor;
63+
this.ctx = ctx;
64+
this.expectationsBlock = expectationsBlock;
65+
}
66+
67+
J.Block rewriteExpectationsBlock() {
68+
List<Statement> newStatements = new ArrayList<>(expectationsBlock.getStatements().size());
69+
for (Statement expectationStatement : expectationsBlock.getStatements()) {
70+
// for each statement, check if it's a method invocation and replace any argument matchers
71+
if (!(expectationStatement instanceof J.MethodInvocation)) {
72+
newStatements.add(expectationStatement);
73+
continue;
74+
}
75+
newStatements.add(rewriteMethodInvocation((J.MethodInvocation) expectationStatement));
76+
}
77+
return expectationsBlock.withStatements(newStatements);
78+
}
79+
80+
private J.MethodInvocation rewriteMethodInvocation(J.MethodInvocation invocation) {
81+
if (invocation.getSelect() instanceof J.MethodInvocation) {
82+
invocation = invocation.withSelect(rewriteMethodInvocation((J.MethodInvocation) invocation.getSelect()));
83+
}
84+
// in mockito, argument matchers must be used for all arguments or none
85+
boolean hasArgumentMatcher = false;
86+
List<Expression> arguments = invocation.getArguments();
87+
for (Expression methodArgument : arguments) {
88+
if (isJmockitArgumentMatcher(methodArgument)) {
89+
hasArgumentMatcher = true;
90+
break;
91+
}
92+
}
93+
// if there are no argument matchers, return the invocation as-is
94+
if (!hasArgumentMatcher) {
95+
return invocation;
96+
}
97+
// replace each argument with the appropriate argument matcher
98+
List<Expression> newArguments = new ArrayList<>(arguments.size());
99+
for (Expression argument : arguments) {
100+
newArguments.add(rewriteMethodArgument(argument));
101+
}
102+
return invocation.withArguments(newArguments);
103+
}
104+
105+
private Expression rewriteMethodArgument(Expression methodArgument) {
106+
String argumentMatcher, template;
107+
if (!isJmockitArgumentMatcher(methodArgument)) {
108+
if (methodArgument instanceof J.Literal) {
109+
return rewritePrimitiveToArgumentMatcher((J.Literal) methodArgument);
110+
} else if (methodArgument instanceof J.Identifier) {
111+
return rewriteIdentifierToArgumentMatcher((J.Identifier) methodArgument);
112+
} else if (methodArgument instanceof J.FieldAccess) {
113+
return rewriteIdentifierToArgumentMatcher(((J.FieldAccess) methodArgument).getName());
114+
} else {
115+
throw new IllegalStateException("Unexpected method argument: " + methodArgument + ", class: " + methodArgument.getClass());
116+
}
117+
}
118+
if (!(methodArgument instanceof J.TypeCast)) {
119+
argumentMatcher = ((J.Identifier) methodArgument).getSimpleName();
120+
template = argumentMatcher + "()";
121+
return applyArgumentTemplate(methodArgument, argumentMatcher, template, new ArrayList<>());
122+
}
123+
return rewriteTypeCastToArgumentMatcher(methodArgument);
124+
}
125+
126+
private Expression applyArgumentTemplate(Expression methodArgument, String argumentMatcher, String template,
127+
List<Object> templateParams) {
128+
visitor.maybeAddImport("org.mockito.Mockito", argumentMatcher);
129+
return JavaTemplate.builder(template)
130+
.javaParser(JavaParser.fromJavaVersion().classpathFromResources(ctx, "mockito-core-3.12"))
131+
.staticImports("org.mockito.Mockito." + argumentMatcher)
132+
.build()
133+
.apply(
134+
new Cursor(visitor.getCursor(), methodArgument),
135+
methodArgument.getCoordinates().replace(),
136+
templateParams.toArray()
137+
);
138+
}
139+
140+
private Expression applyClassArgumentTemplate(Expression methodArgument, String className) {
141+
// rewrite parameter from ((<type>) any) to any(<type>.class)
142+
return JavaTemplate.builder("#{}.class")
143+
.javaParser(JavaParser.fromJavaVersion())
144+
.build()
145+
.apply(
146+
new Cursor(visitor.getCursor(), methodArgument),
147+
methodArgument.getCoordinates().replace(),
148+
className
149+
);
150+
}
151+
152+
private Expression rewriteFullyQualifiedArgument(Expression methodArgument, String className, String fqn) {
153+
String template;
154+
List<Object> templateParams = new ArrayList<>();
155+
String argumentMatcher = "any";
156+
if (MOCKITO_COLLECTION_MATCHERS.containsKey(fqn)) {
157+
// mockito has specific argument matchers for collections
158+
argumentMatcher = MOCKITO_COLLECTION_MATCHERS.get(fqn);
159+
template = argumentMatcher + "()";
160+
} else if (fqn.equals("java.lang.String")) {
161+
argumentMatcher = "anyString";
162+
template = argumentMatcher + "()";
163+
} else {
164+
templateParams.add(applyClassArgumentTemplate(methodArgument, className));
165+
template = "any(#{any(java.lang.Class)})";
166+
}
167+
return applyArgumentTemplate(methodArgument, argumentMatcher, template, templateParams);
168+
}
169+
170+
private Expression rewriteIdentifierToArgumentMatcher(J.Identifier methodArgument) {
171+
if (methodArgument.getType() == null) {
172+
throw new IllegalStateException("Missing type information for identifier: " + methodArgument);
173+
}
174+
if (!(methodArgument.getType() instanceof JavaType.FullyQualified)) {
175+
throw new IllegalStateException("Unexpected identifier type: " + methodArgument.getType());
176+
}
177+
JavaType.FullyQualified type = (JavaType.FullyQualified) methodArgument.getType();
178+
return rewriteFullyQualifiedArgument(methodArgument, type.getClassName(), type.getFullyQualifiedName());
179+
}
180+
181+
private Expression rewriteTypeCastToArgumentMatcher(Expression methodArgument) {
182+
J.TypeCast tc = (J.TypeCast) methodArgument;
183+
String className, fqn;
184+
JavaType typeCastType = tc.getType();
185+
if (typeCastType instanceof JavaType.Parameterized) {
186+
// strip the raw type from the parameterized type
187+
className = ((JavaType.Parameterized) typeCastType).getType().getClassName();
188+
fqn = ((JavaType.Parameterized) typeCastType).getType().getFullyQualifiedName();
189+
} else if (typeCastType instanceof JavaType.Class) {
190+
className = ((JavaType.Class) typeCastType).getClassName();
191+
fqn = ((JavaType.Class) typeCastType).getFullyQualifiedName();
192+
} else {
193+
throw new IllegalStateException("Unexpected J.TypeCast type: " + typeCastType);
194+
}
195+
return rewriteFullyQualifiedArgument(tc, className, fqn);
196+
}
197+
198+
private Expression rewritePrimitiveToArgumentMatcher(J.Literal methodArgument) {
199+
String argumentMatcher;
200+
JavaType.Primitive primitiveType = methodArgument.getType();
201+
switch (Objects.requireNonNull(primitiveType)) {
202+
case Boolean:
203+
argumentMatcher = "anyBoolean";
204+
break;
205+
case Byte:
206+
argumentMatcher = "anyByte";
207+
break;
208+
case Char:
209+
argumentMatcher = "anyChar";
210+
break;
211+
case Double:
212+
argumentMatcher = "anyDouble";
213+
break;
214+
case Float:
215+
argumentMatcher = "anyFloat";
216+
break;
217+
case Int:
218+
argumentMatcher = "anyInt";
219+
break;
220+
case Long:
221+
argumentMatcher = "anyLong";
222+
break;
223+
case Short:
224+
argumentMatcher = "anyShort";
225+
break;
226+
case String:
227+
argumentMatcher = "anyString";
228+
break;
229+
case Null:
230+
argumentMatcher = "isNull";
231+
break;
232+
default:
233+
throw new IllegalStateException("Unexpected primitive type: " + primitiveType);
234+
}
235+
String template = argumentMatcher + "()";
236+
return applyArgumentTemplate(methodArgument, argumentMatcher, template, new ArrayList<>());
237+
}
238+
239+
private static boolean isJmockitArgumentMatcher(Expression expression) {
240+
if (expression instanceof J.TypeCast) {
241+
expression = ((J.TypeCast) expression).getExpression();
242+
}
243+
if (!(expression instanceof J.Identifier)) {
244+
return false;
245+
}
246+
J.Identifier identifier = (J.Identifier) expression;
247+
return JMOCKIT_ARGUMENT_MATCHERS.contains(identifier.getSimpleName());
248+
}
249+
}

0 commit comments

Comments
 (0)