Skip to content

Commit fa95357

Browse files
Fix variable name conflict in JMockit to Mockito migration (#865)
* Fix variable name conflict in JMockit to Mockito migration When migrating JMockit `Expectations` blocks to Mockito, variables declared inside the block that have the same names as variables later in the test method now stay accessible to the generated `when()` calls. ### Changes - Modified `SetupStatementsRewriter` to skip setup statement extraction when there's a variable name conflict - Updated `JMockitBlockRewriter` to track and preserve setup statements within mock invocation groups - Added `wrapStatementsInBlock()` to wrap both setup statements and `when()` calls in the same block - Updated `setupStatementVariableNameConflict` test to reflect the corrected behavior - Added new test `setupStatementVariableUsedInResultAndTestCode` for issue #844 ### Before (broken) ```java { String res = "mockedValue"; } when(config.getValue()).thenReturn(res); // error: res not in scope ``` ### After (fixed) ```java { String res = "mockedValue"; when(config.getValue()).thenReturn(res); } ``` Fixes: - #844 * Polish * Polish
1 parent 5869a99 commit fa95357

File tree

3 files changed

+230
-11
lines changed

3 files changed

+230
-11
lines changed

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

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@
2121
import org.jspecify.annotations.Nullable;
2222
import org.openrewrite.Cursor;
2323
import org.openrewrite.ExecutionContext;
24+
import org.openrewrite.Tree;
2425
import org.openrewrite.java.JavaTemplate;
2526
import org.openrewrite.java.JavaVisitor;
2627
import org.openrewrite.java.tree.*;
28+
import org.openrewrite.marker.Markers;
2729

2830
import java.util.ArrayList;
2931
import java.util.List;
3032

33+
import static java.util.stream.Collectors.toList;
3134
import static org.openrewrite.java.testing.jmockit.JMockitBlockType.*;
3235
import static org.openrewrite.java.testing.jmockit.JMockitUtils.MOCKITO_ALL_IMPORT;
3336
import static org.openrewrite.java.testing.jmockit.JMockitUtils.getJavaParser;
@@ -70,6 +73,10 @@ boolean isRewriteFailed() {
7073
// used with bodyStatementIndex to obtain the coordinates of the next statement to be written
7174
private int numStatementsAdded = 0;
7275

76+
// Track setup statements that need to be preserved and wrapped in a block
77+
private final List<Statement> setupStatementsBeforeFirstMock = new ArrayList<>();
78+
private boolean hasSetupStatements = false;
79+
7380
JMockitBlockRewriter(JavaVisitor<ExecutionContext> visitor, ExecutionContext ctx, J.Block methodBody,
7481
J.NewClass newExpectations, int bodyStatementIndex, JMockitBlockType blockType, int verificationsInOrderIdx) {
7582
this.visitor = visitor;
@@ -102,6 +109,17 @@ J.Block rewriteMethodBody() {
102109
List<J.Identifier> uniqueMocks = new ArrayList<>();
103110
int methodInvocationIdx = -1;
104111
for (Statement jmockitBlockStatement : jmockitBlock.getStatements()) {
112+
// Track setup statements (variable declarations) that appear before mock invocations
113+
if (isSetupStatement(jmockitBlockStatement)) {
114+
hasSetupStatements = true;
115+
if (methodInvocationIdx == -1) {
116+
// Setup statement before the first mock invocation
117+
setupStatementsBeforeFirstMock.add(jmockitBlockStatement);
118+
continue;
119+
}
120+
// Setup statement after a mock invocation - will be handled by the group
121+
}
122+
105123
if (jmockitBlockStatement instanceof J.MethodInvocation) {
106124
J.MethodInvocation invocation = (J.MethodInvocation) jmockitBlockStatement;
107125
Expression select = invocation.getSelect();
@@ -130,6 +148,9 @@ J.Block rewriteMethodBody() {
130148
removeBlock();
131149
}
132150

151+
// Output setup statements that come before the first mock invocation
152+
outputSetupStatements(setupStatementsBeforeFirstMock);
153+
133154
List<Object> mocks = new ArrayList<>(uniqueMocks);
134155
if (isVerificationsInOrder()) {
135156
rewriteInOrderVerify(mocks);
@@ -141,9 +162,61 @@ J.Block rewriteMethodBody() {
141162
if (isFullVerifications()) {
142163
rewriteFullVerify(mocks);
143164
}
165+
166+
// If there were setup statements, wrap the generated statements in a block
167+
// to avoid variable name conflicts with the rest of the method
168+
if (hasSetupStatements) {
169+
wrapStatementsInBlock();
170+
}
171+
144172
return methodBody;
145173
}
146174

175+
private void wrapStatementsInBlock() {
176+
List<Statement> statements = methodBody.getStatements();
177+
if (numStatementsAdded <= 0) {
178+
return;
179+
}
180+
181+
// Get the statements that were added (from bodyStatementIndex to bodyStatementIndex + numStatementsAdded - 1)
182+
int startIdx = bodyStatementIndex;
183+
int endIdx = bodyStatementIndex + numStatementsAdded;
184+
185+
if (startIdx < 0 || endIdx > statements.size()) {
186+
return;
187+
}
188+
189+
// Extract the statements to wrap
190+
List<Statement> statementsToWrap = new ArrayList<>();
191+
for (int i = startIdx; i < endIdx; i++) {
192+
statementsToWrap.add(statements.get(i));
193+
}
194+
195+
// Create a new block containing these statements
196+
J.Block wrapperBlock = new J.Block(
197+
Tree.randomId(),
198+
Space.EMPTY,
199+
Markers.EMPTY,
200+
JRightPadded.build(false),
201+
statementsToWrap.stream()
202+
.map(JRightPadded::build)
203+
.collect(toList()),
204+
Space.EMPTY
205+
);
206+
207+
// Replace the statements with the block
208+
List<Statement> newStatements = new ArrayList<>();
209+
for (int i = 0; i < startIdx; i++) {
210+
newStatements.add(statements.get(i));
211+
}
212+
newStatements.add(wrapperBlock);
213+
for (int i = endIdx; i < statements.size(); i++) {
214+
newStatements.add(statements.get(i));
215+
}
216+
217+
methodBody = methodBody.withStatements(newStatements);
218+
}
219+
147220
private boolean isFullVerifications() {
148221
return this.blockType == FullVerifications;
149222
}
@@ -152,6 +225,11 @@ private boolean isVerificationsInOrder() {
152225
return this.blockType == VerificationsInOrder;
153226
}
154227

228+
private static boolean isSetupStatement(Statement statement) {
229+
// Variable declarations are setup statements
230+
return statement instanceof J.VariableDeclarations;
231+
}
232+
155233
private void rewriteMethodInvocation(List<Statement> statementsToRewrite) {
156234
final MockInvocationResults mockInvocationResults = buildMockInvocationResults(statementsToRewrite);
157235
if (mockInvocationResults == null) {
@@ -169,6 +247,8 @@ private void rewriteMethodInvocation(List<Statement> statementsToRewrite) {
169247

170248
if (!hasResults && !hasTimes && (this.blockType == JMockitBlockType.Expectations || this.blockType.isVerifications())) {
171249
rewriteVerify(invocation, null, "");
250+
// Output setup statements that follow this mock invocation
251+
outputSetupStatements(mockInvocationResults.getSetupStatements());
172252
return;
173253
}
174254
if (mockInvocationResults.getTimes() != null) {
@@ -185,6 +265,15 @@ private void rewriteMethodInvocation(List<Statement> statementsToRewrite) {
185265
if (mockInvocationResults.getMaxTimes() != null) {
186266
rewriteVerify(invocation, mockInvocationResults.getMaxTimes(), "atMost");
187267
}
268+
269+
// Output setup statements that follow this mock invocation
270+
outputSetupStatements(mockInvocationResults.getSetupStatements());
271+
}
272+
273+
private void outputSetupStatements(List<Statement> setupStatements) {
274+
for (Statement setupStatement : setupStatements) {
275+
insertStatementAtCurrentPosition(setupStatement);
276+
}
188277
}
189278

190279
private void removeBlock() {
@@ -196,6 +285,20 @@ private void removeBlock() {
196285
setNextStatementCoordinates(0);
197286
}
198287

288+
private void insertStatementAtCurrentPosition(Statement statement) {
289+
List<Statement> statements = new ArrayList<>(methodBody.getStatements());
290+
int insertIdx = bodyStatementIndex + numStatementsAdded;
291+
if (insertIdx < 0) {
292+
insertIdx = 0;
293+
}
294+
if (insertIdx > statements.size()) {
295+
insertIdx = statements.size();
296+
}
297+
statements.add(insertIdx, statement);
298+
methodBody = methodBody.withStatements(statements);
299+
setNextStatementCoordinates(++numStatementsAdded);
300+
}
301+
199302
private void rewriteResult(J.MethodInvocation invocation, List<Expression> results, boolean hasTimes) {
200303
boolean lenient = this.blockType == NonStrictExpectations && !hasTimes;
201304
String template = getWhenTemplate(results, lenient);
@@ -457,6 +560,15 @@ private static void appendToTemplate(StringBuilder templateBuilder, boolean buil
457560
}
458561
continue;
459562
}
563+
// Skip setup statements (variable declarations) - they're tracked separately
564+
if (isSetupStatement(expectationStatement)) {
565+
resultWrapper.addSetupStatement(expectationStatement);
566+
continue;
567+
}
568+
if (!(expectationStatement instanceof J.Assignment)) {
569+
// unhandled statement type, skip it
570+
continue;
571+
}
460572
J.Assignment assignment = (J.Assignment) expectationStatement;
461573
String variableName = getVariableNameFromAssignment(assignment);
462574
if (variableName == null) {
@@ -525,6 +637,8 @@ private static void appendToTemplate(StringBuilder templateBuilder, boolean buil
525637
private static class MockInvocationResults {
526638
@Setter(AccessLevel.NONE)
527639
private final List<Expression> results = new ArrayList<>();
640+
@Setter(AccessLevel.NONE)
641+
private final List<Statement> setupStatements = new ArrayList<>();
528642
private @Nullable Expression times;
529643
private @Nullable Expression minTimes;
530644
private @Nullable Expression maxTimes;
@@ -533,6 +647,10 @@ private void addResult(Expression result) {
533647
results.add(result);
534648
}
535649

650+
private void addSetupStatement(Statement statement) {
651+
setupStatements.add(statement);
652+
}
653+
536654
private boolean hasAnyTimes() {
537655
return times != null || minTimes != null || maxTimes != null;
538656
}

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

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -87,19 +87,20 @@ J.Block rewriteMethodBody() {
8787
Set<String> methodBodyVariableNames = getVariableNames(methodBody.getStatements());
8888
boolean hasConflict = setupVariableNames.stream().anyMatch(methodBodyVariableNames::contains);
8989

90+
if (hasConflict) {
91+
// When there's a conflict, don't extract setup statements.
92+
// Leave them in the expectations block so JMockitBlockRewriter can handle them
93+
// and wrap everything in a block to avoid variable name conflicts.
94+
continue;
95+
}
96+
9097
// move setup statements before the expectations block
9198
JavaCoordinates coordinates = nc.getCoordinates().before();
9299
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-
}
100+
// move statements individually
101+
for (Statement setupStatement : setupStatements) {
102+
rewriteBodyStatement(setupStatement, coordinates);
103+
coordinates = setupStatement.getCoordinates().after();
103104
}
104105
}
105106

src/test/java/org/openrewrite/java/testing/jmockit/JMockitExpectationsToMockitoTest.java

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.junit.jupiter.api.Disabled;
1919
import org.junit.jupiter.api.Test;
2020
import org.openrewrite.DocumentExample;
21+
import org.openrewrite.Issue;
2122
import org.openrewrite.test.RecipeSpec;
2223
import org.openrewrite.test.RewriteTest;
2324

@@ -1762,8 +1763,8 @@ class MyTest {
17621763
void test() {
17631764
{
17641765
String s = "setup";
1766+
when(myObject.toString()).thenReturn("foo");
17651767
}
1766-
when(myObject.toString()).thenReturn("foo");
17671768
String s = "test";
17681769
}
17691770
}
@@ -1820,4 +1821,103 @@ interface MyInterface {
18201821
)
18211822
);
18221823
}
1824+
1825+
@Issue("https://github.com/openrewrite/rewrite-testing-frameworks/issues/844")
1826+
@Test
1827+
void setupStatementVariableUsedInResultAndTestCode() {
1828+
// Regression test for issue #844 - variables declared in Expectations block should be accessible
1829+
// to when() calls when there's a variable name conflict with test code
1830+
//language=java
1831+
rewriteRun(
1832+
java(
1833+
"""
1834+
class Config {
1835+
private String value;
1836+
private String name;
1837+
1838+
public String getValue() {
1839+
return value;
1840+
}
1841+
1842+
public void setValue(String value) {
1843+
this.value = value;
1844+
}
1845+
1846+
public String getName() {
1847+
return name;
1848+
}
1849+
1850+
public void setName(String name) {
1851+
this.name = name;
1852+
}
1853+
}
1854+
"""
1855+
),
1856+
java(
1857+
"""
1858+
import mockit.Expectations;
1859+
import mockit.Mocked;
1860+
import mockit.integration.junit5.JMockitExtension;
1861+
import org.junit.jupiter.api.extension.ExtendWith;
1862+
1863+
import static org.junit.jupiter.api.Assertions.assertEquals;
1864+
1865+
@ExtendWith(JMockitExtension.class)
1866+
public class SameNameTest {
1867+
@Mocked
1868+
Config config;
1869+
1870+
void testFoo() {
1871+
new Expectations() {{
1872+
String res = "mockedValue";
1873+
config.getValue();
1874+
result = res;
1875+
1876+
String name = "mockedName";
1877+
config.getName();
1878+
result = name;
1879+
}};
1880+
1881+
String res = config.getValue();
1882+
assertEquals("mockedValue", res);
1883+
1884+
String name = config.getName();
1885+
assertEquals("mockedName", name);
1886+
}
1887+
}
1888+
""",
1889+
"""
1890+
import org.junit.jupiter.api.extension.ExtendWith;
1891+
import org.mockito.Mock;
1892+
import org.mockito.junit.jupiter.MockitoExtension;
1893+
1894+
import static org.junit.jupiter.api.Assertions.assertEquals;
1895+
import static org.mockito.Mockito.when;
1896+
1897+
@ExtendWith(MockitoExtension.class)
1898+
public class SameNameTest {
1899+
@Mock
1900+
Config config;
1901+
1902+
void testFoo() {
1903+
{
1904+
String res = "mockedValue";
1905+
when(config.getValue()).thenReturn(res);
1906+
1907+
String name = "mockedName";
1908+
1909+
when(config.getName()).thenReturn(name);
1910+
}
1911+
1912+
String res = config.getValue();
1913+
assertEquals("mockedValue", res);
1914+
1915+
String name = config.getName();
1916+
assertEquals("mockedName", name);
1917+
}
1918+
}
1919+
"""
1920+
)
1921+
);
1922+
}
18231923
}

0 commit comments

Comments
 (0)