Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions astra-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
<version>2.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package org.alfasoftware.astra.core.refactoring.operations.sonar.s6068;

import java.io.IOException;
import java.util.List;

import org.alfasoftware.astra.core.utils.ASTOperation;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.text.edits.MalformedTreeException;

/**
* Abstract base for {@link ASTOperation}s that target the arguments of Mockito
* methods {@code given}, {@code verify}, and {@code when}.
*
* <p>Mirrors the structure of SonarJava's {@code AbstractMockitoArgumentChecker}.
* Handles all five Mockito entry points and extracts the relevant argument list
* before delegating to {@link #visitArguments}.
*
* <p>The five entry points and how their argument lists are reached:
* <ul>
* <li>{@code Mockito.when(foo.method(args))} — the single argument is itself
* the stubbed method call; its argument list is inspected.</li>
* <li>{@code BDDMockito.given(foo.method(args))} — same pattern.</li>
* <li>{@code Mockito.verify(mock).method(args)} — the chained (outer) call
* holds the argument list.</li>
* <li>{@code InOrder.verify(mock).method(args)} — same pattern.</li>
* <li>{@code Stubber.when(mock).method(args)} — same pattern.</li>
* </ul>
*
* <p>Requires Mockito to be present on the JDT classpath so that method
* bindings resolve to their declaring types.
*/
public abstract class AbstractMockitoArgumentCheckOperation implements ASTOperation {

private static final String MOCKITO = "org.mockito.Mockito";
private static final String BDD_MOCKITO = "org.mockito.BDDMockito";
private static final String INORDER = "org.mockito.InOrder";
private static final String STUBBER = "org.mockito.stubbing.Stubber";

@Override
public void run(CompilationUnit compilationUnit, ASTNode node, ASTRewrite rewriter)
throws IOException, MalformedTreeException, BadLocationException {

if (!(node instanceof MethodInvocation)) {
return;
}
MethodInvocation invocation = (MethodInvocation) node;
IMethodBinding binding = invocation.resolveMethodBinding();
if (binding == null) {
return;
}

String declaringType = binding.getDeclaringClass().getQualifiedName();
String methodName = binding.getName();

if (isWhenOrGiven(declaringType, methodName)) {
handleWhenOrGiven(invocation, compilationUnit, rewriter);
} else if (isVerifyOrStubberWhen(declaringType, methodName)) {
handleConsecutiveCall(invocation, compilationUnit, rewriter);
}
}


/**
* For {@code Mockito.when(foo.method(args))} and {@code BDDMockito.given(foo.method(args))},
* the single argument is itself a method call; its arguments are inspected.
*/
@SuppressWarnings("unchecked")
private void handleWhenOrGiven(MethodInvocation invocation, CompilationUnit compilationUnit, ASTRewrite rewriter)
throws IOException, MalformedTreeException, BadLocationException {

List<Expression> whenArgs = invocation.arguments();
if (whenArgs.size() != 1) {
return;
}
Expression arg = skipParentheses(whenArgs.get(0));
if (arg instanceof MethodInvocation mi) {
visitArguments(mi.arguments(), compilationUnit, rewriter);
}
}


/**
* For {@code verify(mock).method(args)}, {@code InOrder.verify(mock).method(args)}, and
* {@code Stubber.when(mock).method(args)}, the chained outer call holds the argument list.
* Mirrors {@code MethodTreeUtils.consecutiveMethodInvocation} in SonarJava.
*/
@SuppressWarnings("unchecked")
private void handleConsecutiveCall(MethodInvocation invocation, CompilationUnit compilationUnit, ASTRewrite rewriter)
throws IOException, MalformedTreeException, BadLocationException {

ASTNode parent = invocation.getParent();
if (parent instanceof MethodInvocation chainedCall &&
// Confirm this invocation is the receiver of the chained call, not an argument to it.
chainedCall.getExpression() == invocation) {
visitArguments(chainedCall.arguments(), compilationUnit, rewriter);
}
}


private boolean isWhenOrGiven(String declaringType, String methodName) {
return MOCKITO.equals(declaringType) && "when".equals(methodName)
|| BDD_MOCKITO.equals(declaringType) && "given".equals(methodName);
}


private boolean isVerifyOrStubberWhen(String declaringType, String methodName) {
return MOCKITO.equals(declaringType) && "verify".equals(methodName)
|| INORDER.equals(declaringType) && "verify".equals(methodName)
|| STUBBER.equals(declaringType) && "when".equals(methodName);
}


/**
* Strips any number of nested {@link ParenthesizedExpression} wrappers.
* Mirrors {@code ExpressionUtils.skipParentheses} in SonarJava.
*/
protected static Expression skipParentheses(Expression expression) {
Expression expr = expression;
while (expr instanceof ParenthesizedExpression parExpr) {
expr = parExpr.getExpression();
}
return expr;
}


/**
* Inspect the argument list of the stubbed or verified method call.
* Implementors should examine the arguments and record any fixes in the
* {@link ASTRewrite} if the rule is violated.
*
* @param arguments the arguments of the method being stubbed or verified
* @param compilationUnit the containing compilation unit
* @param rewriter to record AST rewrites
*/
protected abstract void visitArguments(
List<Expression> arguments, CompilationUnit compilationUnit, ASTRewrite rewriter)
throws IOException, MalformedTreeException, BadLocationException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.alfasoftware.astra.core.refactoring.operations.sonar.s6068;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.text.edits.MalformedTreeException;

/**
* Removes unnecessary {@code eq(...)} argument-matcher wrappers from Mockito
* {@code verify}, {@code when}, and {@code given} calls (java:S6068).
*
* <p>When every argument to the stubbed or verified method is wrapped in
* {@code org.mockito.ArgumentMatchers.eq(...)}, those wrappers are redundant:
* Mockito's default argument matching is already equality-based. This operation
* replaces each {@code eq(x)} call with its inner value {@code x}, producing
* simpler, more readable test code. Unused static imports of {@code eq} are
* cleaned up automatically by {@link org.alfasoftware.astra.core.refactoring.operations.imports.UnusedImportRefactor},
* which runs after every file modification.
*
* <p>The all-eq gate — mirroring SonarJava's {@code MockitoEqSimplificationCheck}
* exactly — means the operation is a no-op when:
* <ul>
* <li>any argument uses a different matcher (e.g. {@code anyString()});</li>
* <li>the method is called with zero arguments;</li>
* <li>method bindings cannot be resolved (Mockito not on the JDT classpath).</li>
* </ul>
*
* <p>Parenthesised {@code eq()} calls such as {@code (eq(x))} are handled:
* the parentheses are stripped before the check and the whole wrapped expression
* is replaced by the inner value.
*
* <p><b>Classpath requirement:</b> Mockito must be included in the
* {@code UseCase}'s additional classpath entries so that JDT can resolve
* {@code org.mockito.ArgumentMatchers}, {@code org.mockito.Mockito}, etc.
*
* <p>Mirrors the detection logic of SonarJava's {@code MockitoEqSimplificationCheck}
* and {@code AbstractMockitoArgumentChecker}.
*/
public class MockitoEqSimplificationRefactor extends AbstractMockitoArgumentCheckOperation {

private static final String ARGUMENT_MATCHERS = "org.mockito.ArgumentMatchers";
/** Deprecated in Mockito 2, removed in Mockito 4 — kept for backwards compatibility. */
private static final String MATCHERS = "org.mockito.Matchers";
/** Mockito extends ArgumentMatchers; eq() may be accessed through Mockito directly. */
private static final String MOCKITO = "org.mockito.Mockito";


@Override
protected void visitArguments(
List<Expression> arguments, CompilationUnit compilationUnit, ASTRewrite rewriter)
throws IOException, MalformedTreeException, BadLocationException {

// Collect the list element (arg) alongside the resolved eq() call (unwrapped).
// We need both: arg is the node to replace; unwrapped is where the inner value lives.
List<Expression> argNodes = new ArrayList<>();
List<MethodInvocation> eqCalls = new ArrayList<>();

for (Expression arg : arguments) {
Expression unwrapped = skipParentheses(arg);
if (unwrapped instanceof MethodInvocation mi && isMockitoEq(mi)) {
argNodes.add(arg);
eqCalls.add(mi);
} else {
// At least one argument is not eq() — mixing matchers with plain values would
// break Mockito at runtime, so we must leave the whole call unchanged.
return;
}
}

if (eqCalls.isEmpty()) {
return; // zero-argument method — nothing to simplify
}

for (int i = 0; i < eqCalls.size(); i++) {
MethodInvocation eq = eqCalls.get(i);
Expression argNode = argNodes.get(i);
@SuppressWarnings("unchecked")
List<Expression> innerArgs = eq.arguments();
// Replace eq(x) — or (eq(x)) if parenthesised — with a copy of x.
rewriter.replace(argNode, rewriter.createCopyTarget(innerArgs.get(0)), null);
}
}


private boolean isMockitoEq(MethodInvocation invocation) {
if (!"eq".equals(invocation.getName().getIdentifier())) {
return false;
}
IMethodBinding binding = invocation.resolveMethodBinding();
if (binding == null) {
return false;
}
String declaringType = binding.getDeclaringClass().getQualifiedName();
return ARGUMENT_MATCHERS.equals(declaringType)
|| MATCHERS.equals(declaringType)
|| MOCKITO.equals(declaringType);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.alfasoftware.astra.core.refactoring.operations.sonar.s6068;

import static org.mockito.ArgumentMatchers.eq;

import org.mockito.BDDMockito;
import org.mockito.Mockito;

public class MockitoEqGivenExample {

interface FooService {
String multiArg(String a, String b);
}

private final FooService foo = Mockito.mock(FooService.class);

void testGivenWithAllEqArgs() {
BDDMockito.given(foo.multiArg(eq("a"), eq("b"))).willReturn("c");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.alfasoftware.astra.core.refactoring.operations.sonar.s6068;

import org.mockito.BDDMockito;
import org.mockito.Mockito;

public class MockitoEqGivenExampleAfter {

interface FooService {
String multiArg(String a, String b);
}

private final FooService foo = Mockito.mock(FooService.class);

void testGivenWithAllEqArgs() {
BDDMockito.given(foo.multiArg("a", "b")).willReturn("c");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.alfasoftware.astra.core.refactoring.operations.sonar.s6068;

import static org.mockito.ArgumentMatchers.eq;

import org.mockito.InOrder;
import org.mockito.Mockito;

public class MockitoEqInOrderVerifyExample {

interface FooService {
String multiArg(String a, String b);
}

private final FooService foo = Mockito.mock(FooService.class);
private final InOrder inOrder = Mockito.inOrder(foo);

void testInOrderVerifyWithAllEqArgs() {
inOrder.verify(foo).multiArg(eq("a"), eq("b"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.alfasoftware.astra.core.refactoring.operations.sonar.s6068;

import org.mockito.InOrder;
import org.mockito.Mockito;

public class MockitoEqInOrderVerifyExampleAfter {

interface FooService {
String multiArg(String a, String b);
}

private final FooService foo = Mockito.mock(FooService.class);
private final InOrder inOrder = Mockito.inOrder(foo);

void testInOrderVerifyWithAllEqArgs() {
inOrder.verify(foo).multiArg("a", "b");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.alfasoftware.astra.core.refactoring.operations.sonar.s6068;

import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;

import org.mockito.Mockito;

public class MockitoEqMixedMatchersExample {

interface FooService {
String multiArg(String a, String b);
}

private final FooService foo = Mockito.mock(FooService.class);

void testMixedMatchersNotSimplified() {
// Not all args are eq() — must not be simplified (mixing would break Mockito)
Mockito.verify(foo).multiArg(eq("a"), anyString());
Mockito.when(foo.multiArg(anyString(), eq("b"))).thenReturn("c");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.alfasoftware.astra.core.refactoring.operations.sonar.s6068;

import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;

import org.mockito.Mockito;

public class MockitoEqMixedMatchersExampleAfter {

interface FooService {
String multiArg(String a, String b);
}

private final FooService foo = Mockito.mock(FooService.class);

void testMixedMatchersNotSimplified() {
// Not all args are eq() — must not be simplified (mixing would break Mockito)
Mockito.verify(foo).multiArg(eq("a"), anyString());
Mockito.when(foo.multiArg(anyString(), eq("b"))).thenReturn("c");
}
}
Loading
Loading