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..a34fb0b --- /dev/null +++ b/astra-core/src/main/java/org/alfasoftware/astra/core/refactoring/operations/sonar/s6068/AbstractMockitoArgumentCheckOperation.java @@ -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}. + * + *

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

+ * + *

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 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 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..30d12cb --- /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.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). + * + *

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

+ * + *

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 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 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); + } +}