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

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

The newest version!
/*
 * SonarQube Java
 * Copyright (C) 2012-2025 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.Optional;
import org.sonar.check.Rule;
import org.sonar.java.checks.helpers.MethodTreeUtils;
import org.sonar.java.checks.helpers.QuickFixHelper;
import org.sonar.java.checks.methods.AbstractMethodDetection;
import org.sonar.java.reporting.InternalJavaIssueBuilder;
import org.sonar.java.reporting.JavaQuickFix;
import org.sonar.java.reporting.JavaTextEdit;
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.semantic.Type;
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.NewArrayTree;
import org.sonar.plugins.java.api.tree.Tree;

@Rule(key = "S3415")
public class AssertionArgumentOrderCheck extends AbstractMethodDetection {

  private static final String ASSERT_ARRAY_EQUALS = "assertArrayEquals";
  private static final String ASSERT_EQUALS = "assertEquals";
  private static final String ASSERT_ITERABLE_EQUALS = "assertIterableEquals";
  private static final String ASSERT_LINES_MATCH = "assertLinesMatch";
  private static final String ASSERT_NOT_EQUALS = "assertNotEquals";
  private static final String ASSERT_NOT_SAME = "assertNotSame";
  private static final String ASSERT_SAME = "assertSame";

  private static final String EXPECTED_VALUE_ACTUAL_VALUE = "expected value, actual value";
  private static final String ACTUAL_VALUE_EXPECTED_VALUE = "actual value, expected value";

  private static final String ORG_JUNIT_ASSERT = "org.junit.Assert";
  private static final String ORG_TESTNG_ASSERT = "org.testng.Assert";
  private static final String ORG_JUNIT5_ASSERTIONS = "org.junit.jupiter.api.Assertions";
  private static final Tree.Kind[] LITERAL_KINDS = {Tree.Kind.STRING_LITERAL, Tree.Kind.INT_LITERAL, Tree.Kind.LONG_LITERAL, Tree.Kind.CHAR_LITERAL,
    Tree.Kind.NULL_LITERAL, Tree.Kind.BOOLEAN_LITERAL, Tree.Kind.DOUBLE_LITERAL, Tree.Kind.FLOAT_LITERAL};
  private static final String MESSAGE_TWO_LITERALS = "Change this assertion to not compare two literals.";
  private static final String MESSAGE_SWAP = "Swap these 2 arguments so they are in the correct order: %s.";
  private static final String MESSAGE_REPLACE = "Replace this literal with the actual expression you want to assert.";

  private static final MethodMatchers COLLECTION_CREATION_CALL = MethodMatchers.or(
    MethodMatchers.create()
      .ofTypes("java.util.Collections")
      .name(name -> name.startsWith("singleton") || name.startsWith("empty"))
      .withAnyParameters()
      .build(),
    MethodMatchers.create().ofTypes("java.util.Arrays").names("asList").withAnyParameters().build());

  @Override
  protected MethodMatchers getMethodInvocationMatchers() {
    return MethodMatchers.or(
      MethodMatchers.create().ofTypes(ORG_JUNIT_ASSERT)
        .names(ASSERT_EQUALS, ASSERT_SAME, ASSERT_NOT_SAME)
        .withAnyParameters()
        .build(),
      // TestNG
      MethodMatchers.create().ofTypes(ORG_TESTNG_ASSERT)
        .names(ASSERT_EQUALS, ASSERT_NOT_EQUALS, ASSERT_SAME, ASSERT_NOT_SAME)
        .withAnyParameters()
        .build(),
      // JUnit 5
      MethodMatchers.create().ofTypes(ORG_JUNIT5_ASSERTIONS)
        .names(ASSERT_ARRAY_EQUALS, ASSERT_EQUALS, ASSERT_ITERABLE_EQUALS, ASSERT_LINES_MATCH, ASSERT_NOT_EQUALS, ASSERT_NOT_SAME, ASSERT_SAME)
        .withAnyParameters()
        .build(),
      // AssertJ
      MethodMatchers.create().ofTypes("org.assertj.core.api.Assertions")
        .names("assertThat", "assertThatObject")
        .addParametersMatcher(MethodMatchers.ANY)
        .build()
    );
  }

  @Override
  protected void onMethodInvocationFound(MethodInvocationTree mit) {
    Type ownerType = mit.methodSymbol().owner().type();
    if (ownerType.is(ORG_JUNIT5_ASSERTIONS)) {
      checkArguments(mit.arguments().get(0), mit.arguments().get(1), EXPECTED_VALUE_ACTUAL_VALUE);
    } else if (ownerType.is(ORG_JUNIT_ASSERT)) {
      ExpressionTree argToCheck = getActualArgument(mit);
      checkArguments(previousArg(argToCheck, mit), argToCheck, EXPECTED_VALUE_ACTUAL_VALUE);
    } else if (ownerType.is(ORG_TESTNG_ASSERT)) {
      checkArguments(mit.arguments().get(1), mit.arguments().get(0), ACTUAL_VALUE_EXPECTED_VALUE);
    } else {
      Optional expectedValue = getExpectedValue(mit);
      ExpressionTree actualValue = mit.arguments().get(0);
      if (expectedValue.isPresent()) {
        checkArguments(expectedValue.get(), actualValue, ACTUAL_VALUE_EXPECTED_VALUE);
      } else {
        checkArgument(actualValue);
      }
    }
  }

  private void checkArguments(ExpressionTree expectedArgument, ExpressionTree actualArgument, String correctOrder) {
    if (actualArgument.is(LITERAL_KINDS)) {
      // When we have a literal as actual, we are sure to have an issue
      if (expectedArgument.is(LITERAL_KINDS)) {
        // no quick-fixes when both are literals... the fix is something else
        newIssue(expectedArgument, actualArgument, MESSAGE_TWO_LITERALS).report();
      } else {
        newIssue(expectedArgument, actualArgument, MESSAGE_SWAP, correctOrder)
          .withQuickFix(() -> swap(expectedArgument, actualArgument))
          .report();
      }
    } else if (isExpectedPattern(actualArgument) && !isExpectedPattern(expectedArgument)) {
      newIssue(expectedArgument, actualArgument, MESSAGE_SWAP, correctOrder)
        .withQuickFix(() -> swap(expectedArgument, actualArgument))
        .report();
    }
  }

  private JavaQuickFix swap(Tree x, Tree y) {
    String newX = QuickFixHelper.contentForTree(y, context);
    String newY = QuickFixHelper.contentForTree(x, context);
    return JavaQuickFix.newQuickFix("Swap arguments")
      .addTextEdit(JavaTextEdit.replaceTree(x, newX))
      .addTextEdit(JavaTextEdit.replaceTree(y, newY))
      .build();
  }

  private void checkArgument(ExpressionTree actualArgument) {
    if (actualArgument.is(LITERAL_KINDS)) {
      // no quick-fixes
      newIssue(actualArgument, MESSAGE_REPLACE).report();
    }
  }

  private InternalJavaIssueBuilder newIssue(ExpressionTree actualArgument, String message, Object... args) {
    return QuickFixHelper.newIssue(context)
      .forRule(this)
      .onTree(actualArgument)
      .withMessage(message, args);
  }

  private InternalJavaIssueBuilder newIssue(ExpressionTree expectedArgument, ExpressionTree actualArgument, String message, Object... args) {
    return newIssue(actualArgument, message, args)
      .withSecondaries(new JavaFileScannerContext.Location("Other argument to swap.", expectedArgument));
  }

  /**
   * Find the related expected value from an assertThat, if the expression is "simple enough":
   * - exactly one subsequent method call
   * - one argument
   */
  private static Optional getExpectedValue(MethodInvocationTree mit) {
    return MethodTreeUtils.consecutiveMethodInvocation(mit)
      .filter(secondInvocation -> {
        Tree parent = secondInvocation.parent();
        return parent != null && parent.is(Tree.Kind.EXPRESSION_STATEMENT) && secondInvocation.arguments().size() == 1;
      })
      .map(secondInvocation -> secondInvocation.arguments().get(0));
  }

  private static boolean isNewArrayWithConstants(ExpressionTree actualArgument) {
    if (actualArgument.is(Tree.Kind.NEW_ARRAY)) {
      NewArrayTree newArrayTree = (NewArrayTree) actualArgument;
      return newArrayTree.initializers().stream().allMatch(AssertionArgumentOrderCheck::isConstant);
    }
    return false;
  }

  private static boolean isCollectionCreationWithConstants(ExpressionTree actualArgument) {
    if (actualArgument.is(Tree.Kind.METHOD_INVOCATION)) {
      MethodInvocationTree mit = (MethodInvocationTree) actualArgument;
      return COLLECTION_CREATION_CALL.matches(mit) && mit.arguments().stream().allMatch(AssertionArgumentOrderCheck::isConstant);
    }
    return false;
  }

  private static ExpressionTree previousArg(ExpressionTree argToCheck, MethodInvocationTree mit) {
    return mit.arguments().get(mit.arguments().indexOf(argToCheck) - 1);
  }

  private static ExpressionTree getActualArgument(MethodInvocationTree mit) {
    int arity = mit.arguments().size();
    ExpressionTree arg = mit.arguments().get(arity - 1);
    // Check for assert equals method with delta
    if (arity > 2 && (arity == 4 || mit.methodSymbol().parameterTypes().stream().allMatch(AssertionArgumentOrderCheck::isDoubleOrFloat))) {
      // last arg is actually delta, take the previous last to get the actual arg.
      arg = mit.arguments().get(arity - 2);
    }
    return arg;
  }

  private static boolean isDoubleOrFloat(Type type) {
    return type.isPrimitive(Type.Primitives.DOUBLE) || type.isPrimitive(Type.Primitives.FLOAT);
  }

  private static boolean isExpectedPattern(ExpressionTree actualArgument) {
    return isConstant(actualArgument) || isNewArrayWithConstants(actualArgument) || isCollectionCreationWithConstants(actualArgument);
  }

  private static boolean isConstant(Tree argToCheck) {
    return argToCheck.is(LITERAL_KINDS)
      || (argToCheck.is(Tree.Kind.IDENTIFIER) && isStaticFinal(((IdentifierTree) argToCheck).symbol()))
      || (argToCheck.is(Tree.Kind.MEMBER_SELECT) && isStaticFinal(((MemberSelectExpressionTree) argToCheck).identifier().symbol()));
  }

  private static boolean isStaticFinal(Symbol symbol) {
    return symbol.isStatic() && symbol.isFinal();
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy