org.sonar.java.checks.tests.AssertJConsecutiveAssertionCheck 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.Collections;
import java.util.List;
import java.util.Optional;
import javax.annotation.Nullable;
import org.sonar.check.Rule;
import org.sonar.java.checks.helpers.ExpressionsHelper;
import org.sonar.java.checks.helpers.MethodTreeUtils;
import org.sonar.java.model.ExpressionUtils;
import org.sonar.java.model.SyntacticEquivalence;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.semantic.MethodMatchers;
import org.sonar.plugins.java.api.semantic.Type;
import org.sonar.plugins.java.api.tree.BlockTree;
import org.sonar.plugins.java.api.tree.ExpressionStatementTree;
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.StatementTree;
import org.sonar.plugins.java.api.tree.Tree;
import static org.sonar.java.checks.helpers.UnitTestUtils.hasTestAnnotation;
@Rule(key = "S5853")
public class AssertJConsecutiveAssertionCheck extends IssuableSubscriptionVisitor {
private static final MethodMatchers ASSERT_THAT_MATCHER = MethodMatchers.create()
.ofSubTypes("org.assertj.core.api.Assertions", "org.assertj.core.api.Assert")
.names("assertThat")
.addParametersMatcher(MethodMatchers.ANY)
.build();
public static final MethodMatchers ASSERTJ_SET_CONTEXT_METHODS = MethodMatchers.create()
.ofSubTypes("org.assertj.core.api.AbstractAssert")
.name(name -> name.startsWith("extracting") || name.startsWith("using") || name.startsWith("filtered")
|| "flatExtracting".equals(name) || "map".equals(name) || "flatMap".equals(name))
.withAnyParameters()
.build();
@Override
public List nodesToVisit() {
return Collections.singletonList(Tree.Kind.METHOD);
}
@Override
public void visitNode(Tree tree) {
MethodTree methodTree = (MethodTree) tree;
if (hasTestAnnotation(methodTree)) {
BlockTree block = methodTree.block();
if (block != null) {
reportConsecutiveAssertions(block.body());
}
}
}
private void reportConsecutiveAssertions(List statements) {
AssertSubject currentSubject = null;
List equivalentInvocations = new ArrayList<>();
for (StatementTree statement : statements) {
Optional assertThatInvocation = getSimpleAssertSubject(statement);
if (assertThatInvocation.isPresent()) {
AssertSubject assertSubject = assertThatInvocation.get();
if (currentSubject == null) {
currentSubject = assertSubject;
} else if (currentSubject.hasEquivalentArgument(assertSubject)) {
equivalentInvocations.add(assertSubject);
} else {
reportIssueIfMultipleCalls(currentSubject, equivalentInvocations);
currentSubject = assertSubject;
equivalentInvocations.clear();
}
} else {
// We have something else than an assertion subject or a subject returning different values between two calls
reportIssueIfMultipleCalls(currentSubject, equivalentInvocations);
currentSubject = null;
equivalentInvocations.clear();
}
}
reportIssueIfMultipleCalls(currentSubject, equivalentInvocations);
}
/**
* A "simple" assertion subject is coming from an assertion chain containing only one assertion predicate
* and the assertion subject argument always returning the same value when called multiple times.
*/
private static Optional getSimpleAssertSubject(StatementTree statement) {
if (statement.is(Tree.Kind.EXPRESSION_STATEMENT)) {
ExpressionTree expression = ((ExpressionStatementTree) statement).expression();
if (expression.is(Tree.Kind.METHOD_INVOCATION)) {
// First method invocation should be an assertion predicate, if not (incomplete assertion), we will not find anything
return getSimpleAssertSubject(((MethodInvocationTree) expression).methodSelect());
}
}
return Optional.empty();
}
private static Optional getSimpleAssertSubject(ExpressionTree expressionTree) {
if (expressionTree.is(Tree.Kind.MEMBER_SELECT)) {
ExpressionTree memberSelectExpression = ((MemberSelectExpressionTree) expressionTree).expression();
if (memberSelectExpression.is(Tree.Kind.METHOD_INVOCATION)) {
MethodInvocationTree mit = (MethodInvocationTree) memberSelectExpression;
if (ASSERT_THAT_MATCHER.matches(mit)) {
ExpressionTree arg = mit.arguments().get(0);
if (ExpressionsHelper.alwaysReturnSameValue(arg)) {
return Optional.of(new AssertSubject(mit, arg));
}
} else if (ASSERTJ_SET_CONTEXT_METHODS.matches(mit)) {
return Optional.empty();
} else {
return getSimpleAssertSubject(mit.methodSelect());
}
}
}
return Optional.empty();
}
private void reportIssueIfMultipleCalls(@Nullable AssertSubject assertSubject, List equivalentAssertions) {
if (assertSubject != null && !equivalentAssertions.isEmpty()) {
reportIssue(assertSubject.methodName(),
"Join these multiple assertions subject to one assertion chain.",
equivalentAssertions.stream().map(AssertSubject::toSecondaryLocation).toList(),
null);
}
}
private static class AssertSubject {
final MethodInvocationTree mit;
final Type assertionType;
final ExpressionTree arg;
AssertSubject(MethodInvocationTree mit, ExpressionTree arg) {
this.mit = mit;
this.assertionType = mit.symbolType().erasure();
this.arg = arg;
}
boolean hasEquivalentArgument(AssertSubject other) {
return SyntacticEquivalence.areEquivalent(arg, other.arg)
&& (other.assertionType.isSubtypeOf(assertionType) || couldBeChained(other));
}
boolean couldBeChained(AssertSubject other) {
return MethodTreeUtils.consecutiveMethodInvocation(other.mit)
.map(chainedNextMethod -> chainedNextMethod.methodSymbol().owner().type().erasure())
.map(mit.methodSymbol().owner().type().erasure()::isSubtypeOf)
.orElse(false);
}
IdentifierTree methodName() {
return ExpressionUtils.methodName(mit);
}
JavaFileScannerContext.Location toSecondaryLocation() {
return new JavaFileScannerContext.Location("Other assertThat", methodName());
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy