From 83b49165a1bb03a0f1868d9f573619f1f36f4f37 Mon Sep 17 00:00:00 2001 From: Joseph Date: Wed, 10 Jun 2026 11:26:50 +0100 Subject: [PATCH 1/2] Add MockitoEqSimplificationRefactor (java:S6068) Introduces MockitoEqSimplificationRefactor and its abstract base AbstractMockitoArgumentCheckOperation, placed under the new org.alfasoftware.astra.core.refactoring.operations.sonar.s6068 package (the convention for all future SonarJava violation ASTOperations). Mirrors SonarJava MockitoEqSimplificationCheck: removes redundant eq(...) wrappers when all arguments to a Mockito when/given/verify call use eq(). Co-Authored-By: Claude Sonnet 4.6 --- astra-core/pom.xml | 6 + ...AbstractMockitoArgumentCheckOperation.java | 147 ++++++++++++++++++ .../MockitoEqSimplificationRefactor.java | 105 +++++++++++++ .../sonar/s6068/MockitoEqGivenExample.java | 19 +++ .../s6068/MockitoEqGivenExampleAfter.java | 17 ++ .../s6068/MockitoEqInOrderVerifyExample.java | 20 +++ .../MockitoEqInOrderVerifyExampleAfter.java | 18 +++ .../s6068/MockitoEqMixedMatchersExample.java | 21 +++ .../MockitoEqMixedMatchersExampleAfter.java | 21 +++ .../s6068/MockitoEqParenthesisedExample.java | 20 +++ .../MockitoEqParenthesisedExampleAfter.java | 18 +++ .../s6068/MockitoEqStaticImportExample.java | 29 ++++ .../MockitoEqStaticImportExampleAfter.java | 28 ++++ .../s6068/MockitoEqStubberWhenExample.java | 23 +++ .../MockitoEqStubberWhenExampleAfter.java | 21 +++ .../sonar/s6068/MockitoEqVerifyExample.java | 23 +++ .../s6068/MockitoEqVerifyExampleAfter.java | 21 +++ .../sonar/s6068/MockitoEqWhenExample.java | 23 +++ .../s6068/MockitoEqWhenExampleAfter.java | 21 +++ .../sonar/s6068/MockitoEqZeroArgsExample.java | 19 +++ .../s6068/MockitoEqZeroArgsExampleAfter.java | 19 +++ .../TestMockitoEqSimplificationRefactor.java | 135 ++++++++++++++++ 22 files changed, 774 insertions(+) create mode 100644 astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/AbstractMockitoArgumentCheckOperation.java create mode 100644 astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqSimplificationRefactor.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqGivenExample.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqGivenExampleAfter.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqInOrderVerifyExample.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqInOrderVerifyExampleAfter.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqMixedMatchersExample.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqMixedMatchersExampleAfter.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqParenthesisedExample.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqParenthesisedExampleAfter.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqStaticImportExample.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqStaticImportExampleAfter.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqStubberWhenExample.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqStubberWhenExampleAfter.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqVerifyExample.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqVerifyExampleAfter.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqWhenExample.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqWhenExampleAfter.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqZeroArgsExample.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqZeroArgsExampleAfter.java create mode 100644 astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/TestMockitoEqSimplificationRefactor.java diff --git a/astra-core/pom.xml b/astra-core/pom.xml index a25b1c6..648dca7 100644 --- a/astra-core/pom.xml +++ b/astra-core/pom.xml @@ -25,6 +25,12 @@ 2.0.1 test + + org.mockito + mockito-core + 5.11.0 + test + diff --git a/astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/AbstractMockitoArgumentCheckOperation.java b/astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/AbstractMockitoArgumentCheckOperation.java new file mode 100644 index 0000000..093cadd --- /dev/null +++ b/astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/AbstractMockitoArgumentCheckOperation.java @@ -0,0 +1,147 @@ +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}. + * + *

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}. + * + *

The five entry points and how their argument lists are reached: + *

    + *
  • {@code Mockito.when(foo.method(args))} — the single argument is itself + * the stubbed method call; its argument list is inspected.
  • + *
  • {@code BDDMockito.given(foo.method(args))} — same pattern.
  • + *
  • {@code Mockito.verify(mock).method(args)} — the chained (outer) call + * holds the argument list.
  • + *
  • {@code InOrder.verify(mock).method(args)} — same pattern.
  • + *
  • {@code Stubber.when(mock).method(args)} — same pattern.
  • + *
+ * + *

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 whenArgs = invocation.arguments(); + if (whenArgs.size() != 1) { + return; + } + Expression arg = skipParentheses(whenArgs.get(0)); + if (arg instanceof MethodInvocation) { + visitArguments(((MethodInvocation) arg).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) { + MethodInvocation chainedCall = (MethodInvocation) parent; + // Confirm this invocation is the receiver of the chained call, not an argument to it. + if (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) { + expr = ((ParenthesizedExpression) expr).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 arguments, CompilationUnit compilationUnit, ASTRewrite rewriter) + throws IOException, MalformedTreeException, BadLocationException; +} diff --git a/astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqSimplificationRefactor.java b/astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqSimplificationRefactor.java new file mode 100644 index 0000000..f3c23f8 --- /dev/null +++ b/astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqSimplificationRefactor.java @@ -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.rewrite.ASTRewrite; +import org.eclipse.jdt.core.dom.Expression; +import org.eclipse.jdt.core.dom.IMethodBinding; +import org.eclipse.jdt.core.dom.MethodInvocation; +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). + * + *

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. + * + *

The all-eq gate — mirroring SonarJava's {@code MockitoEqSimplificationCheck} + * exactly — means the operation is a no-op when: + *

    + *
  • any argument uses a different matcher (e.g. {@code anyString()});
  • + *
  • the method is called with zero arguments;
  • + *
  • method bindings cannot be resolved (Mockito not on the JDT classpath).
  • + *
+ * + *

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. + * + *

Classpath requirement: 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. + * + *

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 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 argNodes = new ArrayList<>(); + List eqCalls = new ArrayList<>(); + + for (Expression arg : arguments) { + Expression unwrapped = skipParentheses(arg); + if (unwrapped instanceof MethodInvocation && isMockitoEq((MethodInvocation) unwrapped)) { + argNodes.add(arg); + eqCalls.add((MethodInvocation) unwrapped); + } 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 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); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqGivenExample.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqGivenExample.java new file mode 100644 index 0000000..617b23b --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqGivenExample.java @@ -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"); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqGivenExampleAfter.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqGivenExampleAfter.java new file mode 100644 index 0000000..b4a45b7 --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqGivenExampleAfter.java @@ -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"); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqInOrderVerifyExample.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqInOrderVerifyExample.java new file mode 100644 index 0000000..ec34aab --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqInOrderVerifyExample.java @@ -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")); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqInOrderVerifyExampleAfter.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqInOrderVerifyExampleAfter.java new file mode 100644 index 0000000..6471d71 --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqInOrderVerifyExampleAfter.java @@ -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"); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqMixedMatchersExample.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqMixedMatchersExample.java new file mode 100644 index 0000000..dec569b --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqMixedMatchersExample.java @@ -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"); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqMixedMatchersExampleAfter.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqMixedMatchersExampleAfter.java new file mode 100644 index 0000000..884abf8 --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqMixedMatchersExampleAfter.java @@ -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"); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqParenthesisedExample.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqParenthesisedExample.java new file mode 100644 index 0000000..f45e549 --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqParenthesisedExample.java @@ -0,0 +1,20 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s6068; + +import static org.mockito.ArgumentMatchers.eq; + +import org.mockito.Mockito; + +public class MockitoEqParenthesisedExample { + + interface FooService { + String multiArg(String a, String b); + int singleArg(int x); + } + + private final FooService foo = Mockito.mock(FooService.class); + + void testParenthesisedEqArgs() { + Mockito.verify(foo).multiArg((eq("a")), (eq("b"))); + Mockito.when(foo.singleArg((eq(7)))).thenReturn(8); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqParenthesisedExampleAfter.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqParenthesisedExampleAfter.java new file mode 100644 index 0000000..5e8661e --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqParenthesisedExampleAfter.java @@ -0,0 +1,18 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s6068; + +import org.mockito.Mockito; + +public class MockitoEqParenthesisedExampleAfter { + + interface FooService { + String multiArg(String a, String b); + int singleArg(int x); + } + + private final FooService foo = Mockito.mock(FooService.class); + + void testParenthesisedEqArgs() { + Mockito.verify(foo).multiArg("a", "b"); + Mockito.when(foo.singleArg(7)).thenReturn(8); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqStaticImportExample.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqStaticImportExample.java new file mode 100644 index 0000000..c58df95 --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqStaticImportExample.java @@ -0,0 +1,29 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s6068; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.mockito.Mockito; + +public class MockitoEqStaticImportExample { + + interface FooService { + String multiArg(String a, String b); + } + + private final FooService foo = Mockito.mock(FooService.class); + + void testStaticWhenWithAllEqArgs() { + when(foo.multiArg(eq("a"), eq("b"))).thenReturn("c"); + } + + void testStaticGivenWithAllEqArgs() { + given(foo.multiArg(eq("a"), eq("b"))).willReturn("c"); + } + + void testStaticVerifyWithAllEqArgs() { + verify(foo).multiArg(eq("a"), eq("b")); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqStaticImportExampleAfter.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqStaticImportExampleAfter.java new file mode 100644 index 0000000..647054c --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqStaticImportExampleAfter.java @@ -0,0 +1,28 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s6068; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.mockito.Mockito; + +public class MockitoEqStaticImportExampleAfter { + + interface FooService { + String multiArg(String a, String b); + } + + private final FooService foo = Mockito.mock(FooService.class); + + void testStaticWhenWithAllEqArgs() { + when(foo.multiArg("a", "b")).thenReturn("c"); + } + + void testStaticGivenWithAllEqArgs() { + given(foo.multiArg("a", "b")).willReturn("c"); + } + + void testStaticVerifyWithAllEqArgs() { + verify(foo).multiArg("a", "b"); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqStubberWhenExample.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqStubberWhenExample.java new file mode 100644 index 0000000..86235ec --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqStubberWhenExample.java @@ -0,0 +1,23 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s6068; + +import static org.mockito.ArgumentMatchers.eq; + +import org.mockito.Mockito; + +public class MockitoEqStubberWhenExample { + + interface FooService { + String multiArg(String a, String b); + int singleArg(int x); + } + + private final FooService foo = Mockito.mock(FooService.class); + + void testDoReturnWhenWithAllEqArgs() { + Mockito.doReturn("result").when(foo).multiArg(eq("a"), eq("b")); + } + + void testDoThrowWhenWithSingleEqArg() { + Mockito.doThrow(new RuntimeException()).when(foo).singleArg(eq(99)); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqStubberWhenExampleAfter.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqStubberWhenExampleAfter.java new file mode 100644 index 0000000..2c2e107 --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqStubberWhenExampleAfter.java @@ -0,0 +1,21 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s6068; + +import org.mockito.Mockito; + +public class MockitoEqStubberWhenExampleAfter { + + interface FooService { + String multiArg(String a, String b); + int singleArg(int x); + } + + private final FooService foo = Mockito.mock(FooService.class); + + void testDoReturnWhenWithAllEqArgs() { + Mockito.doReturn("result").when(foo).multiArg("a", "b"); + } + + void testDoThrowWhenWithSingleEqArg() { + Mockito.doThrow(new RuntimeException()).when(foo).singleArg(99); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqVerifyExample.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqVerifyExample.java new file mode 100644 index 0000000..b247d2d --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqVerifyExample.java @@ -0,0 +1,23 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s6068; + +import static org.mockito.ArgumentMatchers.eq; + +import org.mockito.Mockito; + +public class MockitoEqVerifyExample { + + interface FooService { + String multiArg(String a, String b); + int singleArg(int x); + } + + private final FooService foo = Mockito.mock(FooService.class); + + void testVerifyMultipleEqArgs() { + Mockito.verify(foo).multiArg(eq("a"), eq("b")); + } + + void testVerifySingleEqArg() { + Mockito.verify(foo).singleArg(eq(42)); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqVerifyExampleAfter.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqVerifyExampleAfter.java new file mode 100644 index 0000000..8cf2188 --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqVerifyExampleAfter.java @@ -0,0 +1,21 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s6068; + +import org.mockito.Mockito; + +public class MockitoEqVerifyExampleAfter { + + interface FooService { + String multiArg(String a, String b); + int singleArg(int x); + } + + private final FooService foo = Mockito.mock(FooService.class); + + void testVerifyMultipleEqArgs() { + Mockito.verify(foo).multiArg("a", "b"); + } + + void testVerifySingleEqArg() { + Mockito.verify(foo).singleArg(42); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqWhenExample.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqWhenExample.java new file mode 100644 index 0000000..7e1a2f6 --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqWhenExample.java @@ -0,0 +1,23 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s6068; + +import static org.mockito.ArgumentMatchers.eq; + +import org.mockito.Mockito; + +public class MockitoEqWhenExample { + + interface FooService { + String multiArg(String a, String b); + int singleArg(int x); + } + + private final FooService foo = Mockito.mock(FooService.class); + + void testMultipleEqArgs() { + Mockito.when(foo.multiArg(eq("a"), eq("b"))).thenReturn("c"); + } + + void testSingleEqArg() { + Mockito.when(foo.singleArg(eq(1))).thenReturn(2); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqWhenExampleAfter.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqWhenExampleAfter.java new file mode 100644 index 0000000..3ef0235 --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqWhenExampleAfter.java @@ -0,0 +1,21 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s6068; + +import org.mockito.Mockito; + +public class MockitoEqWhenExampleAfter { + + interface FooService { + String multiArg(String a, String b); + int singleArg(int x); + } + + private final FooService foo = Mockito.mock(FooService.class); + + void testMultipleEqArgs() { + Mockito.when(foo.multiArg("a", "b")).thenReturn("c"); + } + + void testSingleEqArg() { + Mockito.when(foo.singleArg(1)).thenReturn(2); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqZeroArgsExample.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqZeroArgsExample.java new file mode 100644 index 0000000..3ce05ab --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqZeroArgsExample.java @@ -0,0 +1,19 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s6068; + +import org.mockito.Mockito; + +public class MockitoEqZeroArgsExample { + + interface FooService { + void noArgs(); + String noArgsReturnsString(); + } + + private final FooService foo = Mockito.mock(FooService.class); + + void testZeroArgVerifyNotSimplified() { + // Zero-arg method — nothing to simplify + Mockito.verify(foo).noArgs(); + Mockito.when(foo.noArgsReturnsString()).thenReturn("result"); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqZeroArgsExampleAfter.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqZeroArgsExampleAfter.java new file mode 100644 index 0000000..9b59e5a --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqZeroArgsExampleAfter.java @@ -0,0 +1,19 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s6068; + +import org.mockito.Mockito; + +public class MockitoEqZeroArgsExampleAfter { + + interface FooService { + void noArgs(); + String noArgsReturnsString(); + } + + private final FooService foo = Mockito.mock(FooService.class); + + void testZeroArgVerifyNotSimplified() { + // Zero-arg method — nothing to simplify + Mockito.verify(foo).noArgs(); + Mockito.when(foo.noArgsReturnsString()).thenReturn("result"); + } +} diff --git a/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/TestMockitoEqSimplificationRefactor.java b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/TestMockitoEqSimplificationRefactor.java new file mode 100644 index 0000000..25e2e7c --- /dev/null +++ b/astra-core/src/test/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/TestMockitoEqSimplificationRefactor.java @@ -0,0 +1,135 @@ +package org.alfasoftware.astra.core.refactoring.operations.sonar.s6068; + +import java.net.URISyntaxException; +import java.nio.file.Paths; +import java.util.Collections; + +import org.alfasoftware.astra.core.refactoring.AbstractRefactorTest; +import org.junit.Test; +import org.mockito.Mockito; + +/** + * Tests for {@link MockitoEqSimplificationRefactor} (java:S6068). + * + *

Covers all five Mockito entry points, static imports of when/verify/given, + * parenthesised eq() calls, single vs multiple arguments, and no-op cases where + * the rule must not fire (mixed matchers, zero-arg methods). + * + *

Mockito is passed as an explicit JDT classpath entry so that method + * bindings resolve to their declaring types — required for exact rule matching. + */ +public class TestMockitoEqSimplificationRefactor extends AbstractRefactorTest { + + /** + * Path to the mockito-core jar on the local Maven classpath. + * Resolved at runtime from the test classloader so it tracks the pom.xml version. + */ + private static final String MOCKITO_JAR; + static { + try { + MOCKITO_JAR = Paths.get( + Mockito.class.getProtectionDomain().getCodeSource().getLocation().toURI() + ).toString(); + } catch (URISyntaxException e) { + throw new ExceptionInInitializerError(e); + } + } + + private static final String[] MOCKITO_CLASSPATH = new String[]{MOCKITO_JAR}; + + + /** {@code Mockito.when(foo.method(eq(x)))} — all-eq arguments are unwrapped. */ + @Test + public void testWhenAllEqArgs_simplified() { + assertRefactorWithClassPath(MockitoEqWhenExample.class, + Collections.singleton(new MockitoEqSimplificationRefactor()), + MOCKITO_CLASSPATH); + } + + + /** {@code BDDMockito.given(foo.method(eq(x)))} — all-eq arguments are unwrapped. */ + @Test + public void testGivenAllEqArgs_simplified() { + assertRefactorWithClassPath(MockitoEqGivenExample.class, + Collections.singleton(new MockitoEqSimplificationRefactor()), + MOCKITO_CLASSPATH); + } + + + /** {@code Mockito.verify(mock).method(eq(x))} — chained-call pattern, all-eq arguments are unwrapped. */ + @Test + public void testVerifyAllEqArgs_simplified() { + assertRefactorWithClassPath(MockitoEqVerifyExample.class, + Collections.singleton(new MockitoEqSimplificationRefactor()), + MOCKITO_CLASSPATH); + } + + + /** {@code inOrder.verify(mock).method(eq(x))} — InOrder chained-call pattern, all-eq arguments are unwrapped. */ + @Test + public void testInOrderVerifyAllEqArgs_simplified() { + assertRefactorWithClassPath(MockitoEqInOrderVerifyExample.class, + Collections.singleton(new MockitoEqSimplificationRefactor()), + MOCKITO_CLASSPATH); + } + + + /** + * {@code Mockito.doReturn(...).when(mock).method(eq(x))} — Stubber chained-call + * pattern (doReturn, doThrow), all-eq arguments are unwrapped. + */ + @Test + public void testStubberWhenAllEqArgs_simplified() { + assertRefactorWithClassPath(MockitoEqStubberWhenExample.class, + Collections.singleton(new MockitoEqSimplificationRefactor()), + MOCKITO_CLASSPATH); + } + + + /** + * Static imports of {@code when}, {@code given}, and {@code verify} — the operation + * resolves their declaring types via bindings and still simplifies correctly. + * The now-unused static import of {@code eq} is removed. + */ + @Test + public void testStaticImports_simplified() { + assertRefactorWithClassPath(MockitoEqStaticImportExample.class, + Collections.singleton(new MockitoEqSimplificationRefactor()), + MOCKITO_CLASSPATH); + } + + + /** + * {@code (eq(x))} — parenthesised eq() calls are unwrapped after stripping the + * parentheses, and the whole {@code (eq(x))} expression is replaced with just {@code x}. + */ + @Test + public void testParenthesisedEq_simplified() { + assertRefactorWithClassPath(MockitoEqParenthesisedExample.class, + Collections.singleton(new MockitoEqSimplificationRefactor()), + MOCKITO_CLASSPATH); + } + + + /** + * Mixed matchers — when at least one argument uses a non-eq matcher (e.g. anyString()), + * the operation must not simplify any argument (removing eq() would break Mockito at runtime). + */ + @Test + public void testMixedMatchers_notSimplified() { + assertRefactorWithClassPath(MockitoEqMixedMatchersExample.class, + Collections.singleton(new MockitoEqSimplificationRefactor()), + MOCKITO_CLASSPATH); + } + + + /** + * Zero-argument method calls — nothing to simplify; the operation is a no-op. + */ + @Test + public void testZeroArgs_notSimplified() { + assertRefactorWithClassPath(MockitoEqZeroArgsExample.class, + Collections.singleton(new MockitoEqSimplificationRefactor()), + MOCKITO_CLASSPATH); + } +} From 7c69ef9be29c24b3b8567beb05608d7091d5f766 Mon Sep 17 00:00:00 2001 From: Joseph Hoare Date: Wed, 10 Jun 2026 15:44:06 +0100 Subject: [PATCH 2/2] Tweaks --- ...AbstractMockitoArgumentCheckOperation.java | 28 +++++++++---------- .../MockitoEqSimplificationRefactor.java | 6 ++-- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/AbstractMockitoArgumentCheckOperation.java b/astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/AbstractMockitoArgumentCheckOperation.java index 093cadd..a34fb0b 100644 --- a/astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/AbstractMockitoArgumentCheckOperation.java +++ b/astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/AbstractMockitoArgumentCheckOperation.java @@ -80,8 +80,8 @@ private void handleWhenOrGiven(MethodInvocation invocation, CompilationUnit comp return; } Expression arg = skipParentheses(whenArgs.get(0)); - if (arg instanceof MethodInvocation) { - visitArguments(((MethodInvocation) arg).arguments(), compilationUnit, rewriter); + if (arg instanceof MethodInvocation mi) { + visitArguments(mi.arguments(), compilationUnit, rewriter); } } @@ -96,26 +96,24 @@ private void handleConsecutiveCall(MethodInvocation invocation, CompilationUnit throws IOException, MalformedTreeException, BadLocationException { ASTNode parent = invocation.getParent(); - if (parent instanceof MethodInvocation) { - MethodInvocation chainedCall = (MethodInvocation) parent; - // Confirm this invocation is the receiver of the chained call, not an argument to it. - if (chainedCall.getExpression() == invocation) { - visitArguments(chainedCall.arguments(), compilationUnit, rewriter); - } + 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)); + 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)); + return MOCKITO.equals(declaringType) && "verify".equals(methodName) + || INORDER.equals(declaringType) && "verify".equals(methodName) + || STUBBER.equals(declaringType) && "when".equals(methodName); } @@ -125,8 +123,8 @@ private boolean isVerifyOrStubberWhen(String declaringType, String methodName) { */ protected static Expression skipParentheses(Expression expression) { Expression expr = expression; - while (expr instanceof ParenthesizedExpression) { - expr = ((ParenthesizedExpression) expr).getExpression(); + while (expr instanceof ParenthesizedExpression parExpr) { + expr = parExpr.getExpression(); } return expr; } diff --git a/astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqSimplificationRefactor.java b/astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqSimplificationRefactor.java index f3c23f8..30d12cb 100644 --- a/astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqSimplificationRefactor.java +++ b/astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/MockitoEqSimplificationRefactor.java @@ -5,10 +5,10 @@ import java.util.List; import org.eclipse.jdt.core.dom.CompilationUnit; -import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; 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; @@ -64,9 +64,9 @@ protected void visitArguments( for (Expression arg : arguments) { Expression unwrapped = skipParentheses(arg); - if (unwrapped instanceof MethodInvocation && isMockitoEq((MethodInvocation) unwrapped)) { + if (unwrapped instanceof MethodInvocation mi && isMockitoEq(mi)) { argNodes.add(arg); - eqCalls.add((MethodInvocation) unwrapped); + 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.