Skip to content

Commit 0930ff1

Browse files
authored
Merge pull request #10 from picimako/release-030
Changes for release v0.3.0
2 parents 305238a + 027fe42 commit 0930ff1

31 files changed

+1369
-41
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@
44

55
## [Unreleased]
66

7+
## [0.3.0]
8+
- **IntelliJ version support: versions prior to 2021.1 are no longer supported.**
9+
10+
### Added
11+
- Added a new inspection to report checked exceptions in `*Throw()` stubbing calls that are not specified in the stubbed method's `throws` clause.
12+
See [documentation](/docs/stubbing.md#invalid-checked-exception-is-passed-into-throw-methods).
13+
- [#8](https://github.com/picimako/mockitools/issues/8): Added a new inspection that reports multiple consecutive calls on `*Return()` stubbing calls. These can be merged into a single such call.
14+
See [documentation](docs/stubbing.md#consecutive-return-calls-can-be-merged).
15+
16+
### Changed
17+
- Updated Gradle IntelliJ plugin to 1.4.0, gradle to 7.4, and qodana-action to 4.2.5.
18+
- Replaced unit test file checks with a less restrictive, test source root content check, because unit test file names don't necessarily end with the word *Test*.
19+
This will allow certain functionality to run in files whose names don't end with *Test*.
20+
721
## [0.2.0]
822
### Added
923
- [#2](https://github.com/picimako/mockitools/issues/2): Extended `MockTypeInspection` to validate and report types annotated with `@DoNotMock` annotation.
7.93 KB
Loading

docs/stubbing.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Stubbing
2+
3+
## Invalid checked exception is passed into *Throw() methods
4+
5+
![](https://img.shields.io/badge/inspection-orange) ![](https://img.shields.io/badge/since-0.3.0-blue) [![](https://img.shields.io/badge/implementation-ThrowsCheckedExceptionStubbingInspection-blue)](../src/main/java/com/picimako/mockitools/inspection/ThrowsCheckedExceptionStubbingInspection.java)
6+
7+
Reports exception references in <code>*Throw()</code> stubbing methods based on Mockito's rule on checked exceptions.
8+
*If [the specified exception types] contain a checked exception then it has to match one of the checked exceptions of method signature.*
9+
10+
The following constructs are supported:
11+
- `Mockito.when().thenThrow(...)`
12+
- `BDDMockito.given().willThrow(...)`
13+
- `Mockito.doThrow(...).when()`
14+
- `BDDMockito.willThrow(...).given()`
15+
16+
In case of each way of stubbing, further chained <code>*Throw()</code> calls are supported too. In case of an empty list, no problem is reported.
17+
18+
**Example:**
19+
20+
```java
21+
void testMethod() {
22+
MockObject mock = new MockObject();
23+
// IOException is NOT reported because it is in MockObject#doSomething()'s throws list
24+
// IllegalArgumentException is NOT reported because it is not a checked exception
25+
// SqlException IS reported because it is not is in the throws list
26+
Mockito.doThrow(IOException.class, IllegalArgumentException.class, SqlException.class).when(mock).doSomething();
27+
}
28+
29+
class MockObject {
30+
void doSomething() throws IOException, ClassNotFoundException {
31+
}
32+
}
33+
```
34+
35+
## Consecutive `*Return()` calls can be merged
36+
37+
![](https://img.shields.io/badge/inspection-orange) ![](https://img.shields.io/badge/since-0.3.0-blue) [![](https://img.shields.io/badge/implementation-SimplifyConsecutiveStubbingCallsInspection-blue)](../src/main/java/com/picimako/mockitools/inspection/SimplifyConsecutiveStubbingCallsInspection.java)
38+
39+
Reports multiple consecutive calls to `*Return()` methods, so that they may be merge into a single call.
40+
41+
Both `org.mockito.Mockito` and `org.mockito.BDDMockito` based stubbing chains are supported, including calls to `doReturn()`, `thenReturn()` and `willReturn()`.
42+
43+
If there are multiple sections of consecutive calls within the same call chain, they are reported separately for better notification,
44+
but upon invoking the quick fix, all sections are merged respectively. It is always the last consecutive call that is registered.
45+
46+
![consecutive_return_calls](assets/consecutive_return_calls.png)
47+
48+
**Examples:**
49+
50+
```java
51+
From: Mockito.when(mockObject.invoke()).thenReturn(1).thenReturn(2)
52+
to: Mockito.when(mockObject.invoke()).thenReturn(1, 2);
53+
54+
From: Mockito.when(mockObject.invoke()).thenReturn(1).thenCallRealMethod().thenReturn(2).thenReturn(3);
55+
to: Mockito.when(mockObject.invoke()).thenReturn(1).thenCallRealMethod().thenReturn(2, 3);
56+
57+
From: Mockito.when(mockObject.invoke()).thenReturn(1).thenReturn(2).thenCallRealMethod().thenReturn(3);
58+
to: Mockito.when(mockObject.invoke()).thenReturn(1, 2).thenCallRealMethod().thenReturn(3);
59+
60+
From: Mockito.when(mockObject.invoke()).thenReturn(1).thenReturn(2).thenCallRealMethod().thenReturn(3).thenReturn(4);
61+
to: Mockito.when(mockObject.invoke()).thenReturn(1, 2).thenCallRealMethod().thenReturn(3, 4);
62+
63+
From: Mockito.when(mockObject.invoke()).thenReturn(1, 2, 3).thenReturn(4).thenCallRealMethod().thenReturn(5).thenReturn(6, 7);
64+
to: Mockito.when(mockObject.invoke()).thenReturn(1, 2, 3, 4).thenCallRealMethod().thenReturn(5, 6, 7);
65+
```
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//Copyright 2021 Tamás Balog. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2+
3+
package com.picimako.mockitools;
4+
5+
import com.intellij.psi.CommonClassNames;
6+
import com.intellij.psi.PsiClassObjectAccessExpression;
7+
import com.intellij.psi.PsiExpression;
8+
import com.intellij.psi.PsiType;
9+
import com.intellij.psi.util.InheritanceUtil;
10+
import org.jetbrains.annotations.Nullable;
11+
12+
import com.picimako.mockitools.inspection.ClassObjectAccessUtil;
13+
14+
/**
15+
* Utility for exceptions.
16+
*/
17+
public final class ExceptionUtil {
18+
19+
/**
20+
* Returns whether the exception class referenced by the argument expression is a checked exception.
21+
*/
22+
public static boolean isCheckedException(PsiExpression expression) {
23+
return expression instanceof PsiClassObjectAccessExpression
24+
? isCheckedException(ClassObjectAccessUtil.getOperandType(expression))
25+
: isCheckedException(expression.getType());
26+
}
27+
28+
/**
29+
* Returns whether the argument type is a checked exception.
30+
*/
31+
private static boolean isCheckedException(@Nullable PsiType type) {
32+
return !InheritanceUtil.isInheritor(type, CommonClassNames.JAVA_LANG_RUNTIME_EXCEPTION)
33+
&& !InheritanceUtil.isInheritor(type, CommonClassNames.JAVA_LANG_ERROR);
34+
}
35+
36+
private ExceptionUtil() {
37+
//Utility class
38+
}
39+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//Copyright 2021 Tamás Balog. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2+
3+
package com.picimako.mockitools;
4+
5+
import com.intellij.openapi.project.Project;
6+
import com.intellij.psi.JavaPsiFacade;
7+
import com.intellij.psi.PsiElement;
8+
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
9+
10+
/**
11+
* Utility for shortening create expression logic.
12+
*/
13+
public final class ExpressionCreationHelper {
14+
15+
/**
16+
* Creates an expression from the provided expression text.
17+
*/
18+
public static PsiElement createExpressionFromText(String expressionText, PsiElement context, Project project) {
19+
return JavaCodeStyleManager.getInstance(project)
20+
.shortenClassReferences(JavaPsiFacade.getElementFactory(project)
21+
.createExpressionFromText(expressionText, context));
22+
}
23+
24+
private ExpressionCreationHelper() {
25+
//Utility class
26+
}
27+
}

src/main/java/com/picimako/mockitools/ListPopupHelper.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
/*
2-
* Copyright 2021 Tamás Balog. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
3-
*/
1+
//Copyright 2021 Tamás Balog. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
42

53
package com.picimako.mockitools;
64

src/main/java/com/picimako/mockitools/MockitoQualifiedNames.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@ public final class MockitoQualifiedNames {
1010
//Classes
1111
public static final String ORG_MOCKITO_MOCKITO = "org.mockito.Mockito";
1212
public static final String ORG_MOCKITO_BDDMOCKITO = "org.mockito.BDDMockito";
13+
public static final String ORG_MOCKITO_BDD_MY_ONGOING_STUBBING = "org.mockito.BDDMockito.BDDMyOngoingStubbing";
14+
public static final String ORG_MOCKITO_BDD_STUBBER = "org.mockito.BDDMockito.BDDStubber";
1315
public static final String ORG_MOCKITO_ARGUMENT_MATCHERS = "org.mockito.ArgumentMatchers";
1416
public static final String ORG_MOCKITO_ADDITIONAL_MATCHERS = "org.mockito.AdditionalMatchers";
1517
public static final String ORG_MOCKITO_STUBBING_STUBBER = "org.mockito.stubbing.Stubber";
18+
public static final String ORG_MOCKITO_STUBBING_BASESTUBBER = "org.mockito.stubbing.BaseStubber";
19+
public static final String ORG_MOCKITO_ONGOING_STUBBING = "org.mockito.stubbing.OngoingStubbing";
1620
public static final String ORG_MOCKITO_ARGUMENT_CAPTOR = "org.mockito.ArgumentCaptor";
1721
public static final String ORG_MOCKITO_MOCK_SETTINGS = "org.mockito.MockSettings";
1822
public static final String ORG_MOCKITO_MATCHERS = "org.mockito.Matchers";
@@ -68,6 +72,13 @@ public final class MockitoQualifiedNames {
6872
public static final String IGNORE_STUBS = "ignoreStubs";
6973
public static final String VERIFY = "verify";
7074

75+
public static final String THEN_THROW = "thenThrow";
76+
public static final String DO_THROW = "doThrow";
77+
public static final String WILL_THROW = "willThrow";
78+
public static final String THEN_RETURN = "thenReturn";
79+
public static final String DO_RETURN = "doReturn";
80+
public static final String WILL_RETURN = "willReturn";
81+
7182
//MockSettings
7283
public static final String EXTRA_INTERFACES = "extraInterfaces";
7384

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//Copyright 2021 Tamás Balog. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2+
3+
package com.picimako.mockitools;
4+
5+
import static java.util.stream.Collectors.toList;
6+
7+
import java.util.List;
8+
9+
import com.intellij.psi.PsiMethodCallExpression;
10+
import com.intellij.psi.SmartPointerManager;
11+
import com.intellij.psi.SmartPsiElementPointer;
12+
import org.jetbrains.annotations.NotNull;
13+
14+
/**
15+
* Utility for {@link SmartPsiElementPointer}s.
16+
*/
17+
public final class PointersUtil {
18+
19+
public static List<SmartPsiElementPointer<PsiMethodCallExpression>> toPointers(List<PsiMethodCallExpression> calls) {
20+
return calls.stream().map(PointersUtil::toPointer).collect(toList());
21+
}
22+
23+
@NotNull
24+
public static SmartPsiElementPointer<PsiMethodCallExpression> toPointer(PsiMethodCallExpression element) {
25+
return SmartPointerManager.getInstance(element.getProject()).createSmartPsiElementPointer(element, element.getContainingFile());
26+
}
27+
28+
private PointersUtil() {
29+
//Utility class
30+
}
31+
}

src/main/java/com/picimako/mockitools/PsiClassUtil.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
/*
2-
* Copyright 2021 Tamás Balog. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
3-
*/
1+
//Copyright 2021 Tamás Balog. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
42

53
package com.picimako.mockitools;
64

src/main/java/com/picimako/mockitools/PsiMethodUtil.java

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,21 @@
22

33
package com.picimako.mockitools;
44

5+
import static com.siyeh.ig.psiutils.MethodCallUtils.getMethodName;
6+
import static java.util.stream.Collectors.toList;
7+
8+
import java.util.List;
9+
import java.util.Optional;
10+
511
import com.intellij.psi.PsiElement;
612
import com.intellij.psi.PsiExpression;
13+
import com.intellij.psi.PsiExpressionList;
714
import com.intellij.psi.PsiIdentifier;
815
import com.intellij.psi.PsiMethodCallExpression;
916
import com.intellij.psi.PsiReferenceExpression;
17+
import com.intellij.psi.PsiStatement;
1018
import com.intellij.psi.util.PsiTreeUtil;
19+
import com.intellij.util.SmartList;
1120
import org.jetbrains.annotations.NotNull;
1221
import org.jetbrains.annotations.Nullable;
1322

@@ -16,6 +25,10 @@
1625
*/
1726
public final class PsiMethodUtil {
1827

28+
public static boolean isMethodCall(PsiElement call) {
29+
return call instanceof PsiMethodCallExpression;
30+
}
31+
1932
/**
2033
* Returns whether the argument method call has only one argument.
2134
*/
@@ -46,6 +59,26 @@ public static boolean hasSubsequentMethodCall(@NotNull PsiMethodCallExpression m
4659
return methodCall.getParent() instanceof PsiReferenceExpression && methodCall.getParent().getParent() instanceof PsiMethodCallExpression;
4760
}
4861

62+
/**
63+
* Returns the next method call in the chain the argument method call is located.
64+
* <p>
65+
* In the chain {@code variable.doFirst().doSecond()}, for {@code doFirst()} the subsequent call is {@code doSecond()}.
66+
*
67+
* @param methodCall the call to get the next call of.
68+
* @return the next method call, or null if the starting call is null, or if it has no subsequent call
69+
* @since 0.3.0
70+
*/
71+
@Nullable
72+
public static PsiMethodCallExpression getSubsequentMethodCall(@Nullable PsiMethodCallExpression methodCall) {
73+
if (methodCall == null) return null;
74+
PsiElement parent = methodCall.getParent();
75+
if (parent instanceof PsiReferenceExpression) {
76+
PsiElement grandParent = parent.getParent();
77+
if (grandParent instanceof PsiMethodCallExpression) return (PsiMethodCallExpression) grandParent;
78+
}
79+
return null;
80+
}
81+
4982
/**
5083
* Gets the arguments of the provided method call, given that the argument list exists and is not null.
5184
*/
@@ -67,6 +100,11 @@ public static PsiExpression getFirstArgument(@NotNull PsiMethodCallExpression me
67100
public static PsiExpression getQualifier(@NotNull PsiMethodCallExpression methodCall) {
68101
return methodCall.getMethodExpression().getQualifierExpression();
69102
}
103+
104+
@Nullable
105+
public static PsiElement getReferenceNameElement(PsiMethodCallExpression methodCall) {
106+
return methodCall.getMethodExpression().getReferenceNameElement();
107+
}
70108

71109
/**
72110
* Deletes the arguments of the provided method call.
@@ -96,6 +134,78 @@ public static boolean isIdentifierOfMethodCall(PsiElement element) {
96134
&& element.getParent().getParent() instanceof PsiMethodCallExpression;
97135
}
98136

137+
/**
138+
* Finds the method call in a chain named by the argument {@code methodNameToFind}, upwards from {@code aCallInChain}.
139+
* <p>
140+
* For a chain {@code variable.doFirst().doSecond().doThird().doFourth()}, if the method is called with {@code PsiExpression[doThird()], "doFirst"}
141+
* arguments, it will find the first call, but would not find e.g. {@code doFourth()}.
142+
*
143+
* @param aCallInChain the starting point in the chain
144+
* @param methodNameToFind the method name to find
145+
* @return the found method call, or empty optional if none found
146+
* @since 0.3.0
147+
*/
148+
public static Optional<PsiMethodCallExpression> findCallUpwardsInChain(@NotNull PsiExpression aCallInChain, String methodNameToFind) {
149+
PsiElement current = aCallInChain;
150+
while (current.getFirstChild() instanceof PsiReferenceExpression) {
151+
PsiElement previousCall = current.getFirstChild().getFirstChild();
152+
if (isMethodCall(previousCall)) {
153+
var prevCall = (PsiMethodCallExpression) previousCall;
154+
if (methodNameToFind.equals(getMethodName(prevCall))) {
155+
return Optional.of(prevCall);
156+
}
157+
current = previousCall;
158+
} else break;
159+
}
160+
return Optional.empty();
161+
}
162+
163+
/**
164+
* Finds the method call in a chain named by the argument {@code methodNameToFind}, downwards from {@code aCallInChain}.
165+
* <p>
166+
* For a chain {@code variable.doFirst().doSecond().doThird().doFourth()}, if the method is called with {@code PsiExpression[doSecond()], "doFourth"}
167+
* arguments, it will find the last call, but would not find e.g. {@code doSecond()}.
168+
*
169+
* @param aCallInChain the starting point in the chain
170+
* @param methodNameToFind the method name to find
171+
* @return the found method call, or empty optional if none found
172+
* @since 0.3.0
173+
*/
174+
public static Optional<PsiMethodCallExpression> findCallDownwardsInChain(@NotNull PsiExpression aCallInChain, String methodNameToFind) {
175+
var callsInChain = PsiTreeUtil.collectParents(aCallInChain,
176+
PsiMethodCallExpression.class, false, e -> e instanceof PsiExpressionList || e instanceof PsiStatement);
177+
return callsInChain.stream()
178+
.filter(call -> methodNameToFind.equals(getMethodName(call)))
179+
.findFirst();
180+
}
181+
182+
// /**
183+
// * Collects the method calls from the call chain in which the provided call is the last one.
184+
// * <p>
185+
// * Thus, the calls will be in a reverse order compared to the order they are actually called.
186+
// */
187+
// public static List<PsiMethodCallExpression> collectCallsInChain(@NotNull PsiExpression lastCallInChain) {
188+
// var calls = new SmartList<PsiElement>(lastCallInChain);
189+
// PsiElement current = lastCallInChain;
190+
// while (current.getFirstChild() instanceof PsiReferenceExpression) {
191+
// PsiElement previousCall = current.getFirstChild().getFirstChild();
192+
// if (previousCall instanceof PsiMethodCallExpression) {
193+
// calls.add(previousCall);
194+
// current = previousCall;
195+
// } else break;
196+
// }
197+
// return calls.stream().map(PsiMethodCallExpression.class::cast).collect(toList());
198+
// }
199+
200+
public static List<PsiMethodCallExpression> collectCallsInChainFromFirst(PsiMethodCallExpression expression, boolean includeMySelf) {
201+
return PsiTreeUtil.collectParents(expression,
202+
PsiMethodCallExpression.class, includeMySelf, e -> e instanceof PsiExpressionList || e instanceof PsiStatement);
203+
}
204+
205+
public static List<PsiMethodCallExpression> collectCallsInChainFromFirst(PsiMethodCallExpression expression) {
206+
return collectCallsInChainFromFirst(expression, false);
207+
}
208+
99209
private PsiMethodUtil() {
100210
//Utility class
101211
}

0 commit comments

Comments
 (0)