
org.sonar.java.checks.tests.MockitoArgumentMatchersUsedOnAllParametersCheck Maven / Gradle / Ivy
/*
* SonarQube Java
* Copyright (C) 2012-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
package org.sonar.java.checks.tests;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.sonar.check.Rule;
import org.sonar.java.model.ExpressionUtils;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.semantic.MethodMatchers;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.tree.Arguments;
import org.sonar.plugins.java.api.tree.BaseTreeVisitor;
import org.sonar.plugins.java.api.tree.BlockTree;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.IdentifierTree;
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.ReturnStatementTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.TypeCastTree;
@Rule(key = "S6073")
public class MockitoArgumentMatchersUsedOnAllParametersCheck extends AbstractMockitoArgumentChecker {
private static final String ARGUMENT_CAPTOR_CLASS = "org.mockito.ArgumentCaptor";
private static final String ARGUMENT_MATCHER_CLASS = "org.mockito.ArgumentMatchers";
private static final String ADDITIONAL_MATCHER_CLASS = "org.mockito.AdditionalMatchers";
private static final String OLD_MATCHER_CLASS = "org.mockito.Matchers";
private static final String TOP_MOCKITO_CLASS = "org.mockito.Mockito";
// Argument matchers are not filtered on names but the class they originate from to support the addition of new matchers.
private static final MethodMatchers ARGUMENT_MARCHER = MethodMatchers.create()
.ofTypes(ARGUMENT_MATCHER_CLASS, ADDITIONAL_MATCHER_CLASS, OLD_MATCHER_CLASS, TOP_MOCKITO_CLASS)
.anyName()
.withAnyParameters()
.build();
private static final MethodMatchers HAMCREST_ADAPTOR_MATCHER = MethodMatchers.create()
.ofTypes("org.mockito.hamcrest.MockitoHamcrest")
.anyName()
.withAnyParameters()
.build();
private static final MethodMatchers ARGUMENT_CAPTOR = MethodMatchers.create()
.ofTypes(ARGUMENT_CAPTOR_CLASS)
.names("capture")
.addWithoutParametersMatcher()
.build();
@Override
public void leaveFile(JavaFileScannerContext context) {
MethodVisitor.cachedResults.clear();
}
@Override
protected void visitArguments(Arguments arguments) {
if (arguments.isEmpty()) {
return;
}
List nonMatchers = new ArrayList<>();
for (ExpressionTree arg : arguments) {
arg = ExpressionUtils.skipParentheses(arg);
if (!isArgumentMatcherLike(arg)) {
nonMatchers.add(arg);
}
}
int nonMatchersFound = nonMatchers.size();
if (!nonMatchers.isEmpty() && nonMatchersFound < arguments.size()) {
String primaryMessage = String.format(
"Add an \"eq()\" argument matcher on %s",
nonMatchersFound == 1 ? "this parameter." : "these parameters."
);
reportIssue(nonMatchers.get(0),
primaryMessage,
nonMatchers.stream()
.skip(1)
.map(secondary -> new JavaFileScannerContext.Location("", secondary))
.toList(),
null);
}
}
private static boolean isArgumentMatcherLike(ExpressionTree tree) {
ExpressionTree unpacked = skipCasts(tree);
if (!unpacked.is(Tree.Kind.METHOD_INVOCATION)) {
return false;
}
MethodInvocationTree invocation = (MethodInvocationTree) unpacked;
return ARGUMENT_CAPTOR.matches(invocation) ||
HAMCREST_ADAPTOR_MATCHER.matches(invocation) ||
ARGUMENT_MARCHER.matches(invocation) ||
returnsAnArgumentMatcher(invocation);
}
/**
* Test whether an invoked method eventually returns an argument matcher by checking if all its return paths lead to another method invocation.
* The return method invocations are not checked as they are most likely stored in some testing helper out of the file under analysis.
*
* @param invocation The method invocation to explore
* @return Whether the method invoked returns something that could be an argument matcher
*/
private static boolean returnsAnArgumentMatcher(MethodInvocationTree invocation) {
ExpressionTree methodSelect = invocation.methodSelect();
IdentifierTree identifier;
if (methodSelect.is(Tree.Kind.MEMBER_SELECT)) {
identifier = ((MemberSelectExpressionTree) methodSelect).identifier();
} else {
// If not a member select, then it must be an identifier
identifier = (IdentifierTree) methodSelect;
}
Symbol symbol = identifier.symbol();
if (symbol.isUnknown()) {
return true;
}
Symbol.MethodSymbol methodSymbol = (Symbol.MethodSymbol) symbol;
MethodTree declaration = methodSymbol.declaration();
if (declaration == null) {
return false;
}
MethodVisitor methodVisitor = new MethodVisitor();
declaration.accept(methodVisitor);
return methodVisitor.onlyReturnsMethodInvocations;
}
/**
* Pop the chained casts to return an expression.
*
* @param tree Chained casts
* @return The expression behind the last cast in the chain
*/
private static ExpressionTree skipCasts(ExpressionTree tree) {
ExpressionTree current = ExpressionUtils.skipParentheses(tree);
while (current.is(Tree.Kind.TYPE_CAST)) {
TypeCastTree cast = (TypeCastTree) current;
current = ExpressionUtils.skipParentheses(cast.expression());
}
return current;
}
private static class MethodVisitor extends BaseTreeVisitor {
static Map cachedResults = new HashMap<>();
boolean onlyReturnsMethodInvocations = false;
@Override
public void visitMethod(MethodTree tree) {
if (cachedResults.containsKey(tree)) {
onlyReturnsMethodInvocations = cachedResults.get(tree);
return;
}
cachedResults.put(tree, Boolean.FALSE);
BlockTree block = tree.block();
if (block == null) {
// If the method is abstract, we assume its potential implementations only return method invocations.
cachedResults.put(tree, Boolean.TRUE);
return;
}
onlyReturnsMethodInvocations = block.body().stream()
.filter(statement -> statement.is(Tree.Kind.RETURN_STATEMENT))
.map(ReturnStatementTree.class::cast)
.map(ReturnStatementTree::expression)
.allMatch(expression -> expression.is(Tree.Kind.METHOD_INVOCATION));
cachedResults.put(tree, onlyReturnsMethodInvocations);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy