All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sonar.java.checks.tests.AssertionsCompletenessCheck Maven / Gradle / Ivy

There is a newer version: 8.6.0.37351
Show newest version
/*
 * 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.List;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.sonar.check.Rule;
import org.sonar.java.annotations.VisibleForTesting;
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.JavaFileScannerContext.Location;
import org.sonar.plugins.java.api.semantic.MethodMatchers;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.SymbolMetadata;
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.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.Modifier;
import org.sonar.plugins.java.api.tree.NewClassTree;
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;

import static java.util.Collections.emptyList;
import static org.sonar.java.checks.helpers.UnitTestUtils.hasTestAnnotation;
import static org.sonar.plugins.java.api.semantic.MethodMatchers.ANY;

@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 ASSERTJ_THROWABLE_SUPERTYPE = "org.assertj.core.api.ThrowableTypeAssert";
  private static final String TRUTH_SUPERTYPE = "com.google.common.truth.TestVerb";
  private static final String JAVA6_ABSTRACT_SOFT_ASSERT = "org.assertj.core.api.Java6AbstractStandardSoftAssertions";
  private static final MethodMatchers MOCKITO_VERIFY = MethodMatchers.create()
    .ofSubTypes("org.mockito.Mockito").names("verify").withAnyParameters().build();
  private static final MethodMatchers ASSERTJ_ASSERT_ALL =
    MethodMatchers.create()
      .ofSubTypes("org.assertj.core.api.SoftAssertions", "org.assertj.core.api.Java6SoftAssertions")
      .names("assertAll")
      .withAnyParameters()
      .build();
  private static final MethodMatchers ASSERTJ_ASSERT_THAT = MethodMatchers.create()
    .ofSubTypes("org.assertj.core.api.AbstractSoftAssertions").name(name -> name.startsWith("assertThat"))
    .withAnyParameters()
    .build();
  private static final MethodMatchers ASSERTJ_ASSERT_SOFTLY = MethodMatchers.create()
    .ofSubTypes("org.assertj.core.api.SoftAssertions").names("assertSoftly").withAnyParameters().build();

  private static final MethodMatchers FEST_LIKE_ASSERT_THAT = MethodMatchers.or(
    MethodMatchers.create()
      .ofSubTypes(
        // Fest 1.X
        "org.fest.assertions.Assertions",
        // Fest 2.X
        "org.fest.assertions.api.Assertions",
        // AssertJ 1.X
        "org.assertj.core.api.AbstractSoftAssertions",
        // AssertJ 2.X
        "org.assertj.core.api.Assertions",
        "org.assertj.core.api.Java6Assertions",
        "org.assertj.core.api.AbstractStandardSoftAssertions",
        JAVA6_ABSTRACT_SOFT_ASSERT,
        // AssertJ 3.X
        "org.assertj.core.api.StrictAssertions")
      .names("assertThat")
      .addParametersMatcher(ANY)
      .build(),

    MethodMatchers.create()
      .ofTypes(
        // AssertJ 3.X having specific method names
        "org.assertj.core.api.Assertions")
      .names(
        "assertThatObject",
        "assertThatCode",
        "assertThatExceptionOfType",
        "assertThatNullPointerException",
        "assertThatIllegalArgumentException",
        "assertThatIOException",
        "assertThatIllegalStateException")
      .withAnyParameters()
      .build(),

    MethodMatchers.create()
      .ofSubTypes(
        // Truth 0.29
        "com.google.common.truth.Truth",
        // Truth8 0.39
        "com.google.common.truth.Truth8").name(name -> name.startsWith("assert"))
      .addParametersMatcher(ANY)
      .build());

  private static final MethodMatchers ASSERTJ_BDD_EXCEPTION_ASSERTIONS = MethodMatchers.or(
    MethodMatchers.create()
      .ofTypes(
        "org.assertj.core.api.BDDAssertions")
      .names(
        "thenNullPointerException",
        "thenIllegalArgumentException",
        "thenIOException",
        "thenIllegalStateException",
        "thenException",
        "thenIndexOutOfBoundsException",
        "thenReflectiveOperationException",
        "thenRuntimeException")
      .withAnyParameters()
      .build());

  private static final Pattern FEST_LIKE_EXCLUSION_NAMES = Pattern.compile("as.*+|using.*+|with.*+|describedAs|overridingErrorMessage|extracting");

  private static final MethodMatchers FEST_LIKE_EXCLUSIONS = MethodMatchers.or(
    MethodMatchers.create()
      .ofSubTypes(FEST_ASSERT_SUPERTYPE, ASSERTJ_SUPERTYPE, ASSERTJ_THROWABLE_SUPERTYPE)
      .name(name -> FEST_LIKE_EXCLUSION_NAMES.matcher(name).matches())
      .withAnyParameters()
      .build(),

    // Truth has assertWithMessage, Truth8 does not
    MethodMatchers.create().ofSubTypes(TRUTH_SUPERTYPE).names("that").withAnyParameters().build()
  );

  private Boolean chainedToAnyMethodButFestExclusions = null;
  private JavaFileScannerContext context;

  private static boolean isMethodCalledOnJava6AbstractStandardSoftAssertions(MethodInvocationTree mit) {
    // Java6AbstractStandardSoftAssertions does not contain 'assertAll()' method so this class should not be used
    ExpressionTree methodSelect = mit.methodSelect();
    if (methodSelect.is(Tree.Kind.MEMBER_SELECT)) {
      Type type = ((MemberSelectExpressionTree) methodSelect).expression().symbolType();
      if (type.is(JAVA6_ABSTRACT_SOFT_ASSERT)) {
        return true;
      }
    }
    return false;
  }

  @Override
  public void scanFile(final JavaFileScannerContext context) {
    if (context.getSemanticModel() == null) {
      // requires semantic
      return;
    }
    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;
    }
    super.visitMethod(methodTree);

    // soft assertions are allowed to be incomplete outside unit tests
    if (hasTestAnnotation(methodTree)) {
      SoftAssertionsVisitor softAssertionsVisitor = new SoftAssertionsVisitor();
      methodTree.accept(softAssertionsVisitor);
      if (softAssertionsVisitor.assertThatCalled && !isInClassUsingSoftAssertionsExtension(methodTree.symbol())) {
        context.reportIssue(this, methodTree.block().closeBraceToken(), "Add a call to 'assertAll' after all 'assertThat'.");
      }
    }
  }

  private static boolean isInClassUsingSoftAssertionsExtension(Symbol symbol) {
    Symbol owner = symbol.owner();
    if (owner != null && owner.isTypeSymbol()) {
      List annotationValues = owner.metadata().valuesForAnnotation("org.junit.jupiter.api.extension.ExtendWith");
      return (annotationValues != null && annotationValues.stream().anyMatch(av -> isSoftAssertionsExtensionClass(av.value())))
        // Check if nesting class uses SoftAssertionsExtension since it is a valid use case.
        || isInClassUsingSoftAssertionsExtension(owner);
    }
    return false;
  }

  @VisibleForTesting
  static boolean isSoftAssertionsExtensionClass(Object value) {
    if (value instanceof Object[] values) {
      for (Object v : values) {
        if (v instanceof Symbol vSymbol && vSymbol.type().is("org.assertj.core.api.junit.jupiter.SoftAssertionsExtension")) {
          return true;
        }
      }
    }
    return false;
  }

  @Override
  public void visitMethodInvocation(MethodInvocationTree mit) {
    if (incompleteAssertion(mit)) {
      return;
    }
    Boolean previous = chainedToAnyMethodButFestExclusions;
    chainedToAnyMethodButFestExclusions = ((chainedToAnyMethodButFestExclusions != null) && chainedToAnyMethodButFestExclusions) || !FEST_LIKE_EXCLUSIONS.matches(mit);
    scan(mit.methodSelect());
    // skip arguments
    chainedToAnyMethodButFestExclusions = previous;
  }

  private boolean incompleteAssertion(MethodInvocationTree mit) {
    if (isMethodCalledOnJava6AbstractStandardSoftAssertions(mit)) {
      return false;
    }

    if ((FEST_LIKE_ASSERT_THAT.matches(mit) || MOCKITO_VERIFY.matches(mit) || ASSERTJ_BDD_EXCEPTION_ASSERTIONS.matches(mit)) &&
      !Boolean.TRUE.equals(chainedToAnyMethodButFestExclusions)) {
      context.reportIssue(this, mit.methodSelect(), "Complete the assertion.");
      return true;
    }
    return false;
  }

  class SoftAssertionsVisitor extends BaseTreeVisitor {
    private boolean assertThatCalled;
    private final List intermediateMethodInvocations;

    public SoftAssertionsVisitor() {
      this(false, emptyList());
    }

    public SoftAssertionsVisitor(boolean assertThatCalled, List intermediateMethodInvocations) {
      this.assertThatCalled = assertThatCalled;
      this.intermediateMethodInvocations = intermediateMethodInvocations;
    }

    @Override
    public void visitNewClass(NewClassTree tree) {
      if (tree.symbolType().is(JAVA6_ABSTRACT_SOFT_ASSERT)) {
        context.reportIssue(AssertionsCompletenessCheck.this, tree, "Use 'Java6SoftAssertions' instead of 'Java6AbstractStandardSoftAssertions'.");
        return;
      }
      super.visitNewClass(tree);
    }

    @Override
    public void visitMethodInvocation(MethodInvocationTree mit) {
      if (isMethodCalledOnJava6AbstractStandardSoftAssertions(mit)) {
        return;
      }

      boolean assertThatStateBeforeInvocation = assertThatCalled;
      super.visitMethodInvocation(mit);

      if (ASSERTJ_ASSERT_SOFTLY.matches(mit)) {
        assertThatCalled = assertThatStateBeforeInvocation;
      }

      if (ASSERTJ_ASSERT_ALL.matches(mit)) {
        if (assertThatCalled) {
          assertThatCalled = false;
        } else {
          List allLocations = Stream.concat(intermediateMethodInvocations.stream(), Stream.of(mit)).toList();
          MethodInvocationTree mainLocation = allLocations.get(0);
          List secondaries = allLocations.stream()
            .skip(1L)
            .map(methodInvocation -> new Location("", methodInvocation.methodSelect()))
            .toList();
          context.reportIssue(AssertionsCompletenessCheck.this, mainLocation, "Add one or more 'assertThat' before 'assertAll'.", secondaries, null);
        }
      } else if (ASSERTJ_ASSERT_THAT.matches(mit) && !isJUnitSoftAssertions(mit)) {
        assertThatCalled = true;
      } else if (mit.methodSymbol().declaration() != null && intermediateMethodInvocations.stream().noneMatch(invocation -> invocation.methodSymbol().equals(mit.methodSymbol()))) {
        List allLocations = Stream.concat(intermediateMethodInvocations.stream(), Stream.of(mit)).toList();
        SoftAssertionsVisitor softAssertionsVisitor = new SoftAssertionsVisitor(assertThatCalled, allLocations);
        mit.methodSymbol().declaration().accept(softAssertionsVisitor);
        assertThatCalled = softAssertionsVisitor.assertThatCalled;
      }
    }

    @Override
    public void visitTryStatement(TryStatementTree tree) {
      boolean hasAutoCloseableSoftAssertion = tree.resourceList().stream()
        .map(this::resourceSymbol)
        .map(Symbol::type)
        .filter(Objects::nonNull)
        .anyMatch(type -> type.isSubtypeOf("org.assertj.core.api.AutoCloseableSoftAssertions"));
      super.visitTryStatement(tree);
      if (hasAutoCloseableSoftAssertion) {
        if (assertThatCalled) {
          assertThatCalled = false;
        } else {
          List secondaries = intermediateMethodInvocations.stream()
            .map(methodInvocation -> new Location("", methodInvocation.methodSelect()))
            .toList();
          context.reportIssue(AssertionsCompletenessCheck.this,
            tree.block().closeBraceToken(),
            "Add one or more 'assertThat' before the end of this try block.",
            secondaries,
            null);
        }
      }
    }

    private Symbol resourceSymbol(Tree tree) {
      switch (tree.kind()) {
        case VARIABLE:
          return ((VariableTree) tree).symbol();
        case IDENTIFIER:
          return ((IdentifierTree) tree).symbol();
        case MEMBER_SELECT:
          return ((MemberSelectExpressionTree) tree).identifier().symbol();
        default:
          throw new IllegalArgumentException("Tree is not try-with-resources resource");
      }
    }

    private 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;
    }

  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy