/* * SonarQube Java * Copyright (C) 2012-2016 SonarSource SA * mailto:contact AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * 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 GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonar.java.checks; import com.google.common.base.MoreObjects; import java.util.ArrayDeque; import java.util.Deque; import java.util.Objects; import org.sonar.check.Rule; import org.sonar.java.matcher.MethodMatcher; import org.sonar.java.matcher.MethodMatcherCollection; import org.sonar.java.matcher.NameCriteria; import org.sonar.java.matcher.TypeCriteria; import org.sonar.java.model.ModifiersUtils; import org.sonar.plugins.java.api.JavaFileScanner; import org.sonar.plugins.java.api.JavaFileScannerContext; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.semantic.Type; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.ExpressionTree; 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.Modifier; import org.sonar.plugins.java.api.tree.ReturnStatementTree; import org.sonar.plugins.java.api.tree.Tree; import org.sonar.plugins.java.api.tree.TryStatementTree; import org.sonar.plugins.java.api.tree.VariableTree; @Rule(key = "S2970") public class AssertionsCompletenessCheck extends BaseTreeVisitor implements JavaFileScanner { private static final String FEST_ASSERT_SUPERTYPE = "org.fest.assertions.Assert"; private static final String ASSERTJ_SUPERTYPE = "org.assertj.core.api.AbstractAssert"; private static final String TRUTH_SUPERTYPE = "com.google.common.truth.TestVerb"; private static final MethodMatcher MOCKITO_VERIFY = MethodMatcher.create() .typeDefinition("org.mockito.Mockito").name("verify").withAnyParameters(); private static final MethodMatcher ASSERTJ_ASSERT_ALL = MethodMatcher.create() .typeDefinition(TypeCriteria.subtypeOf("org.assertj.core.api.SoftAssertions")).name("assertAll").withAnyParameters(); private static final MethodMatcher ASSERTJ_ASSERT_THAT = MethodMatcher.create() .typeDefinition(TypeCriteria.subtypeOf("org.assertj.core.api.AbstractSoftAssertions")) .name(NameCriteria.startsWith("assertThat")) .withAnyParameters(); private static final MethodMatcherCollection FEST_LIKE_ASSERT_THAT = MethodMatcherCollection.create( // Fest 1.X assertThatOnType("org.fest.assertions.Assertions"), // Fest 2.X assertThatOnType("org.fest.assertions.api.Assertions"), // AssertJ 1.X assertThatOnType("org.assertj.core.api.AbstractSoftAssertions"), // AssertJ 2.X assertThatOnType("org.assertj.core.api.Assertions"), assertThatOnType("org.assertj.core.api.AbstractStandardSoftAssertions"), // AssertJ 3.X assertThatOnType("org.assertj.core.api.StrictAssertions"), // Truth 0.29 methodWithName("com.google.common.truth.Truth", NameCriteria.startsWith("assert")) ); private static final MethodMatcherCollection FEST_LIKE_EXCLUSIONS = MethodMatcherCollection.create( methodWithName(FEST_ASSERT_SUPERTYPE, NameCriteria.startsWith("as")), methodWithName(FEST_ASSERT_SUPERTYPE, NameCriteria.startsWith("using")), methodWithName(FEST_ASSERT_SUPERTYPE, NameCriteria.startsWith("with")), methodWithName(FEST_ASSERT_SUPERTYPE, NameCriteria.is("describedAs")), methodWithName(FEST_ASSERT_SUPERTYPE, NameCriteria.is("overridingErrorMessage")), methodWithName(ASSERTJ_SUPERTYPE, NameCriteria.startsWith("as")), methodWithName(ASSERTJ_SUPERTYPE, NameCriteria.startsWith("using")), methodWithName(ASSERTJ_SUPERTYPE, NameCriteria.startsWith("with")), methodWithName(ASSERTJ_SUPERTYPE, NameCriteria.is("describedAs")), methodWithName(ASSERTJ_SUPERTYPE, NameCriteria.is("overridingErrorMessage")), methodWithName(TRUTH_SUPERTYPE, NameCriteria.is("that")) ); private Boolean chainedToAnyMethodButFestExclusions = null; private JavaFileScannerContext context; private final Deque<Boolean> containsAssertThatWithoutAssertAll = new ArrayDeque<>(); private static MethodMatcher assertThatOnType(String type) { return MethodMatcher.create().typeDefinition(type).name("assertThat").addParameter(TypeCriteria.anyType()); } private static MethodMatcher methodWithName(String superType, NameCriteria nameCriteria) { return MethodMatcher.create().typeDefinition(TypeCriteria.subtypeOf(superType)).name(nameCriteria).withAnyParameters(); } @Override public void scanFile(final JavaFileScannerContext context) { this.context = context; scan(context.getTree()); } @Override public void visitVariable(VariableTree tree) { // skip variable assignments } @Override public void visitReturnStatement(ReturnStatementTree tree) { // skip return statements } @Override public void visitMethod(MethodTree methodTree) { if (ModifiersUtils.hasModifier(methodTree.modifiers(), Modifier.ABSTRACT)) { return; } containsAssertThatWithoutAssertAll.push(false); super.visitMethod(methodTree); if (Boolean.TRUE.equals(containsAssertThatWithoutAssertAll.pop())) { context.reportIssue(this, methodTree.block().closeBraceToken(), "Add a call to 'assertAll' after all 'assertThat'."); } } @Override public void visitMethodInvocation(MethodInvocationTree mit) { checkForAssertJSoftAssertions(mit); if (incompleteAssertion(mit)) { return; } Boolean previous = chainedToAnyMethodButFestExclusions; chainedToAnyMethodButFestExclusions = MoreObjects.firstNonNull(chainedToAnyMethodButFestExclusions, false) || !FEST_LIKE_EXCLUSIONS.anyMatch(mit); scan(mit.methodSelect()); // skip arguments chainedToAnyMethodButFestExclusions = previous; } @Override public void visitTryStatement(TryStatementTree tree) { boolean hasAutoCloseableSoftAssertion = tree.resources().stream() .map(VariableTree::symbol) .map(Symbol::type) .filter(Objects::nonNull) .filter(type -> type.isSubtypeOf("org.assertj.core.api.AutoCloseableSoftAssertions")) .findFirst() .isPresent(); super.visitTryStatement(tree); if (hasAutoCloseableSoftAssertion) { checkAssertJAssertAll(tree.block().closeBraceToken(), "Add one or more 'assertThat' before the end of this try block."); } } private void checkForAssertJSoftAssertions(MethodInvocationTree mit) { if (ASSERTJ_ASSERT_ALL.matches(mit)) { checkAssertJAssertAll(mit.methodSelect(), "Add one or more 'assertThat' before 'assertAll'."); } else if (ASSERTJ_ASSERT_THAT.matches(mit) && !isJUnitSoftAssertions(mit)) { set(containsAssertThatWithoutAssertAll, true); } } private void checkAssertJAssertAll(Tree issueLocation, String issueMessage) { if (Boolean.TRUE.equals(containsAssertThatWithoutAssertAll.peek())) { set(containsAssertThatWithoutAssertAll, false); } else { context.reportIssue(this, issueLocation, issueMessage); } } private static boolean isJUnitSoftAssertions(MethodInvocationTree mit) { ExpressionTree expressionTree = mit.methodSelect(); if (expressionTree.is(Tree.Kind.MEMBER_SELECT)) { Type type = ((MemberSelectExpressionTree) expressionTree).expression().symbolType(); return type.isSubtypeOf("org.assertj.core.api.JUnitSoftAssertions") || type.isSubtypeOf("org.assertj.core.api.Java6JUnitSoftAssertions"); } return false; } private static void set(Deque<Boolean> collection, boolean value) { if (Boolean.TRUE.equals(collection.peek()) != value) { collection.pop(); collection.push(value); } } private boolean incompleteAssertion(MethodInvocationTree mit) { if (((FEST_LIKE_ASSERT_THAT.anyMatch(mit) && (mit.arguments().size() == 1)) || MOCKITO_VERIFY.matches(mit)) && !Boolean.TRUE.equals(chainedToAnyMethodButFestExclusions)) { context.reportIssue(this, mit.methodSelect(), "Complete the assertion."); return true; } return false; } }