Skip to content

Commit

Permalink
Issue#475 Initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Kamil-Gabaydullin committed Mar 15, 2023
1 parent 06c8b16 commit 26a04b9
Show file tree
Hide file tree
Showing 2 changed files with 291 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package tech.picnic.errorprone.bugpatterns;

import static com.google.errorprone.BugPattern.LinkType.CUSTOM;
import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION;
import static com.google.errorprone.BugPattern.StandardTags.SIMPLIFICATION;
import static java.util.stream.Collectors.joining;
import static tech.picnic.errorprone.bugpatterns.util.Documentation.BUG_PATTERNS_BASE_URL;
import static tech.picnic.errorprone.bugpatterns.util.MoreJUnitMatchers.TEST_METHOD;

import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.code.Symbol;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.mockito.Mockito;

/**
* A {@link BugChecker} that flags multiple usages of {@link Mockito#verifyNoInteractions} in favor
* of one call with varargs.
*
* <p>Multiple calls of {@link Mockito#verifyNoInteractions} can make the code more verbose than
* necessary. Instead of multiple calls, because {@link Mockito#verifyNoInteractions} accepts
* varargs, one call should be preferred.
*/
@AutoService(BugChecker.class)
@BugPattern(
summary = "Prefer one call to `verifyNoInteractions(varargs...)` over multiple calls",
link = BUG_PATTERNS_BASE_URL + "MockitoVerifyNoInteractionsUsage",
linkType = CUSTOM,
severity = SUGGESTION,
tags = SIMPLIFICATION)
public final class MockitoVerifyNoInteractionsUsage extends BugChecker
implements MethodTreeMatcher {
private static final long serialVersionUID = 1L;

@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
boolean isTestMethod = TEST_METHOD.matches(tree, state);
if (!isTestMethod) {
return Description.NO_MATCH;
}
var verifyNoInteractionsInvocations = getVerifyNoInteractionsInvocations(tree);
if (verifyNoInteractionsInvocations.size() < 2) {
return Description.NO_MATCH;
}
String combinedArgument =
verifyNoInteractionsInvocations.stream()
.map(MethodInvocationTree::getArguments)
.flatMap(List::stream)
.map(Object::toString)
.collect(joining(", "));

SuggestedFix.Builder fixBuilder = SuggestedFix.builder();
MethodInvocationTree lastInvocation =
verifyNoInteractionsInvocations.get(verifyNoInteractionsInvocations.size() - 1);
verifyNoInteractionsInvocations.forEach(
invocationTree -> {
if (!invocationTree.equals(lastInvocation)) {
fixBuilder.replace(
ASTHelpers.getStartPosition(invocationTree),
state.getEndPosition(invocationTree) + 1,
"");
}
});
fixBuilder.replace(lastInvocation, "verifyNoInteractions(" + combinedArgument + ")");

return describeMatch(tree, fixBuilder.build());
}

private static ImmutableList<MethodInvocationTree> getVerifyNoInteractionsInvocations(
MethodTree methodTree) {
ImmutableList.Builder<MethodInvocationTree> invocationTreeBuilder = ImmutableList.builder();

new TreeScanner<@Nullable Void, @Nullable Void>() {
@Override
public @Nullable Void visitMethodInvocation(
MethodInvocationTree node, @Nullable Void unused) {
Symbol symbol = ASTHelpers.getSymbol(node);
if (symbol.getSimpleName().toString().equals("verifyNoInteractions")) {
invocationTreeBuilder.add(node);
}
return super.visitMethodInvocation(node, unused);
}
}.scan(methodTree, null);

return invocationTreeBuilder.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package tech.picnic.errorprone.bugpatterns;

import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;

final class MockitoVerifyNoInteractionsUsageTest {
private final BugCheckerRefactoringTestHelper refactoringTestHelper =
BugCheckerRefactoringTestHelper.newInstance(
MockitoVerifyNoInteractionsUsage.class, getClass());

@Test
void identification() {
CompilationTestHelper.newInstance(MockitoVerifyNoInteractionsUsage.class, getClass())
.addSourceLines(
"A.java",
"import static org.mockito.Mockito.mock;",
"import static org.mockito.Mockito.verifyNoInteractions;",
"",
"import org.junit.jupiter.api.Test;",
"",
"class A {",
" @Test",
" void a() {}",
" @Test",
" void b() {",
" Runnable runnable = mock(Runnable.class);",
" verifyNoInteractions(runnable);",
" }",
" @Test",
" void c() {",
" Runnable runnable = mock(Runnable.class);",
" Runnable runnable1 = mock(Runnable.class);",
" verifyNoInteractions(runnable, runnable1);",
" }",
" @Test",
" // BUG: Diagnostic contains:",
" void d() {",
" Runnable runnable1 = mock(Runnable.class);",
" Runnable runnable2 = mock(Runnable.class);",
" verifyNoInteractions(runnable1);",
" verifyNoInteractions(runnable2);",
" }",
" @Test",
" // BUG: Diagnostic contains:",
" void e() {",
" Runnable runnable1 = mock(Runnable.class);",
" verifyNoInteractions(runnable1);",
" Runnable runnable2 = mock(Runnable.class);",
" verifyNoInteractions(runnable2);",
" }",
" @Test",
" // BUG: Diagnostic contains:",
" void f() {",
" Runnable runnable1 = mock(Runnable.class);",
" verifyNoInteractions(runnable1);",
" Runnable runnable2 = mock(Runnable.class);",
" Runnable runnable3 = mock(Runnable.class);",
" verifyNoInteractions(runnable2, runnable3);",
" Runnable runnable4 = mock(Runnable.class);",
" Runnable runnable5 = mock(Runnable.class);",
" verifyNoInteractions(runnable4);",
" verifyNoInteractions(runnable5);",
" }",
"}")
.doTest();
}

@Test
void replaceSequentialCalls() {
refactoringTestHelper
.addInputLines(
"A.java",
"import static org.mockito.Mockito.mock;",
"import static org.mockito.Mockito.verifyNoInteractions;",
"",
"import org.junit.jupiter.api.Test;",
"",
"class A {",
" @Test",
" void m() {",
" Runnable runnable1 = mock(Runnable.class);",
" Runnable runnable2 = mock(Runnable.class);",
" verifyNoInteractions(runnable1);",
" verifyNoInteractions(runnable2);",
" }",
"}")
.addOutputLines(
"A.java",
"import static org.mockito.Mockito.mock;",
"import static org.mockito.Mockito.verifyNoInteractions;",
"",
"import org.junit.jupiter.api.Test;",
"",
"class A {",
" @Test",
" void m() {",
" Runnable runnable1 = mock(Runnable.class);",
" Runnable runnable2 = mock(Runnable.class);",
"",
" verifyNoInteractions(runnable1, runnable2);",
" }",
"}")
.doTest(TestMode.TEXT_MATCH);
}

@Test
void replaceNonSequentialCalls() {
refactoringTestHelper
.addInputLines(
"A.java",
"import static org.mockito.Mockito.mock;",
"import static org.mockito.Mockito.verifyNoInteractions;",
"",
"import org.junit.jupiter.api.Test;",
"",
"class A {",
" @Test",
" void m() {",
" Runnable runnable1 = mock(Runnable.class);",
" verifyNoInteractions(runnable1);",
" Runnable runnable2 = mock(Runnable.class);",
" verifyNoInteractions(runnable2);",
" }",
"}")
.addOutputLines(
"A.java",
"import static org.mockito.Mockito.mock;",
"import static org.mockito.Mockito.verifyNoInteractions;",
"",
"import org.junit.jupiter.api.Test;",
"",
"class A {",
" @Test",
" void m() {",
" Runnable runnable1 = mock(Runnable.class);",
"",
" Runnable runnable2 = mock(Runnable.class);",
" verifyNoInteractions(runnable1, runnable2);",
" }",
"}")
.doTest(TestMode.TEXT_MATCH);
}

@Test
void replaceManyCalls() {
refactoringTestHelper
.addInputLines(
"A.java",
"import static org.mockito.Mockito.mock;",
"import static org.mockito.Mockito.verifyNoInteractions;",
"",
"import org.junit.jupiter.api.Test;",
"",
"class A {",
" @Test",
" void m() {",
" Runnable runnable1 = mock(Runnable.class);",
" verifyNoInteractions(runnable1);",
" Runnable runnable2 = mock(Runnable.class);",
" Runnable runnable3 = mock(Runnable.class);",
" verifyNoInteractions(runnable2, runnable3);",
" Runnable runnable4 = mock(Runnable.class);",
" Runnable runnable5 = mock(Runnable.class);",
" verifyNoInteractions(runnable4);",
" verifyNoInteractions(runnable5);",
" }",
"}")
.addOutputLines(
"A.java",
"import static org.mockito.Mockito.mock;",
"import static org.mockito.Mockito.verifyNoInteractions;",
"",
"import org.junit.jupiter.api.Test;",
"",
"class A {",
" @Test",
" void m() {",
" Runnable runnable1 = mock(Runnable.class);",
"",
" Runnable runnable2 = mock(Runnable.class);",
" Runnable runnable3 = mock(Runnable.class);",
"",
" Runnable runnable4 = mock(Runnable.class);",
" Runnable runnable5 = mock(Runnable.class);",
"",
" verifyNoInteractions(runnable1, runnable2, runnable3, runnable4, runnable5);",
" }",
"}")
.doTest(TestMode.TEXT_MATCH);
}
}

0 comments on commit 26a04b9

Please sign in to comment.