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

org.sonar.java.checks.ReverseSequencedCollectionCheck 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;

import java.util.EnumSet;
import java.util.Set;
import javax.annotation.Nullable;
import org.sonar.check.Rule;
import org.sonar.java.checks.methods.AbstractMethodDetection;
import org.sonar.java.model.ExpressionUtils;
import org.sonar.plugins.java.api.JavaVersion;
import org.sonar.plugins.java.api.JavaVersionAwareVisitor;
import org.sonar.plugins.java.api.semantic.MethodMatchers;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.Symbol.VariableSymbol;
import org.sonar.plugins.java.api.tree.Arguments;
import org.sonar.plugins.java.api.tree.AssignmentExpressionTree;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.ForEachStatement;
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.NewClassTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.VariableTree;

@Rule(key = "S6877")
public class ReverseSequencedCollectionCheck extends AbstractMethodDetection implements JavaVersionAwareVisitor {

  private static final String MESSAGE = "Remove this \"reverse\" statement and replace \"%s\" with \"%s.reversed()\" after.";

  private static final String ADD_ALL = "addAll";

  private static final MethodMatchers LIST_CONSTRUCTORS = MethodMatchers.create()
    .ofTypes(
      "java.util.ArrayList",
      "java.util.LinkedList",
      "java.util.Vector",
      "java.util.Stack",
      "java.util.concurrent.CopyOnWriteArrayList",
      "javax.management.AttributeList",
      "javax.management.relation.RoleList",
      "javax.management.relation.RoleUnresolvedList")
    .constructor()
    .withAnyParameters()
    .build();

  private static final MethodMatchers LIST_READONLY_CONSUMERS = MethodMatchers.create()
    .ofSubTypes("java.util.List")
    .names(ADD_ALL, "copyOf")
    .addParametersMatcher("java.util.Collection")
    .build();

  private static final MethodMatchers LIST_READ_ACCESSORS = MethodMatchers.or(
    MethodMatchers.create()
      .ofAnyType()
      .names("clone", "getClass", "getFirst", "getLast", "hashCode", "isEmpty", "listIterator", "parallelStream",
        "size", "spliterator", "stream", "toArray", "toString", "wait", "notify", "notifyAll")
      .addWithoutParametersMatcher()
      .build(),
    MethodMatchers.create()
      .ofAnyType()
      .names("contains", "containsAll", "equals", "forEach", "get", "indexOf", "lastIndexOf", "listIterator", "toArray")
      .addParametersMatcher(MethodMatchers.ANY)
      .build());

  private static final MethodMatchers LIST_WRITE_ACCESSORS = MethodMatchers.or(
    MethodMatchers.create()
      .ofAnyType()
      .names("clear", "removeFirst", "removeLast")
      .addWithoutParametersMatcher()
      .build(),
    MethodMatchers.create()
      .ofAnyType()
      .names("add", ADD_ALL, "addFirst", "addLast", "remove", "removeAll", "replaceAll", "retainAll", "sort")
      .addParametersMatcher(MethodMatchers.ANY)
      .build(),
    MethodMatchers.create()
      .ofAnyType()
      .names("add", ADD_ALL, "set")
      .addParametersMatcher("int", MethodMatchers.ANY)
      .build());

  private static final Set SUPPORTED_REVERSE_SCOPE_CHILD_KINDS = EnumSet.of(
    Tree.Kind.IDENTIFIER,
    Tree.Kind.ARGUMENTS,
    Tree.Kind.METHOD_INVOCATION,
    Tree.Kind.EXPRESSION_STATEMENT,
    Tree.Kind.MEMBER_SELECT,
    Tree.Kind.BLOCK,
    Tree.Kind.IF_STATEMENT);

  private static final Set SUPPORTED_USAGE_SCOPE_CHILD_KINDS = EnumSet.of(
    Tree.Kind.IDENTIFIER,
    Tree.Kind.MEMBER_SELECT,
    Tree.Kind.METHOD_INVOCATION,
    Tree.Kind.ASSIGNMENT,
    Tree.Kind.EXPRESSION_STATEMENT,
    Tree.Kind.BLOCK,
    Tree.Kind.IF_STATEMENT);

  @Override
  public boolean isCompatibleWithJavaVersion(JavaVersion version) {
    return version.isJava21Compatible();
  }

  @Override
  protected MethodMatchers getMethodInvocationMatchers() {
    return MethodMatchers.create()
      .ofTypes("java.util.Collections")
      .names("reverse")
      .addParametersMatcher("java.util.List")
      .build();
  }

  @Override
  protected void onMethodInvocationFound(MethodInvocationTree methodInvocation) {
    Arguments reverseMethodArguments = methodInvocation.arguments();
    if (reverseMethodArguments.isEmpty() || !reverseMethodArguments.get(0).is(Tree.Kind.IDENTIFIER)) {
      return;
    }
    IdentifierTree reverseMethodArgument = (IdentifierTree) reverseMethodArguments.get(0);
    Symbol symbol = reverseMethodArgument.symbol();
    // Only support local variables. "Collections.reverse(list)" mutates "list" and return void. If "list" was a
    // field or a method parameter, we don't know if the mutation of the "list" is used outside of this method body. So
    // we don't raise issue.
    if (!symbol.isLocalVariable() || symbol.isParameter()) {
      return;
    }
    VariableSymbol reverseMethodArgumentSymbol = (VariableSymbol) symbol;
    if (areUsagesCompatibleWithReversed(reverseMethodArgument, reverseMethodArgumentSymbol)) {
      String message = String.format(MESSAGE, reverseMethodArgumentSymbol.name(), reverseMethodArgumentSymbol.name());
      reportIssue(ExpressionUtils.methodName(methodInvocation), message);
    }
  }

  private static boolean areUsagesCompatibleWithReversed(IdentifierTree reverseArgument, VariableSymbol listSymbol) {
    VariableTree declaration = listSymbol.declaration();
    // We are checking that there are no write modifiers of "list" after "Collections.reverse(list)".
    // Because we don't use symbolic execution, we will know that a usage is after by using its position in the file.
    // This strategy only works if there are no loops or lambdas. To ensure this, we will check that "Collections.reverse(list)"
    // and its write usages share the same "reverseParentScope".
    Tree reverseParentScope = findParentScope(reverseArgument, SUPPORTED_REVERSE_SCOPE_CHILD_KINDS);
    if (declaration == null || reverseParentScope == null ||
      !isInitializerCompatibleWithReversed(declaration, reverseParentScope)) {
      return false;
    }
    for (IdentifierTree usage : listSymbol.usages()) {
      if (!isUsageCompatibleWithReversed(usage, reverseArgument, reverseParentScope)) {
        return false;
      }
    }
    return true;
  }

  private static boolean isInitializerCompatibleWithReversed(VariableTree declaration, Tree reverseParentScope) {
    if (declaration.parent() instanceof ForEachStatement) {
      return false;
    }
    ExpressionTree initializer = declaration.initializer();
    if (initializer == null) {
      return true;
    }
    return isNullOrListConstructor(initializer) &&
      matchReverseParentSafeScope(declaration.parent(), reverseParentScope);
  }

  private static boolean isUsageCompatibleWithReversed(IdentifierTree usage, IdentifierTree reverseArgument, Tree reverseParentScope) {
    if (usage == reverseArgument || isCompatibleReadUsage(usage)) {
      return true;
    }
    if (isAfter(usage, reverseArgument)) {
      return false;
    }
    return isCompatibleWriteUsage(usage) && matchReverseParentSafeScope(usage, reverseParentScope);
  }

  private static boolean isCompatibleWriteUsage(IdentifierTree usage) {
    if (matchAccessor(usage, LIST_WRITE_ACCESSORS)) {
      return true;
    }
    return usage.parent() instanceof AssignmentExpressionTree assignmentExpression &&
      isNullOrListConstructor(assignmentExpression.expression());
  }

  private static boolean isCompatibleReadUsage(IdentifierTree usage) {
    if (matchAccessor(usage, LIST_READ_ACCESSORS)) {
      return true;
    }
    Tree parent = usage.parent();
    if (parent instanceof Arguments arguments) {
      return isListReadOnlyConsumer(arguments.parent());
    } else {
      return parent instanceof ForEachStatement forEachStatement && forEachStatement.expression() == usage;
    }
  }

  /**
   * @return true if there is a method call on the given "usage" identifier, e.g. "list.getFirst()", and if the
   * method call matches the given "methodMatchers".
   */
  private static boolean matchAccessor(IdentifierTree usage, MethodMatchers methodMatchers) {
    Tree parent = usage.parent();
    if (parent instanceof MemberSelectExpressionTree memberSelect && memberSelect.expression() == usage) {
      Tree grandParent = parent.parent();
      if (grandParent instanceof MethodInvocationTree methodInvocation && methodInvocation.methodSelect() == parent) {
        return methodMatchers.matches(methodInvocation);
      }
    }
    return false;
  }

  private static boolean isAfter(IdentifierTree a, IdentifierTree b) {
    return a.identifierToken().range().start().isAfter(b.identifierToken().range().start());
  }

  private static boolean isNullOrListConstructor(ExpressionTree expression) {
    if (expression.is(Tree.Kind.NULL_LITERAL)) {
      return true;
    }
    return isListConstructor(expression);
  }

  private static boolean isListReadOnlyConsumer(@Nullable Tree tree) {
    if (tree instanceof MethodInvocationTree methodInvocation) {
      return LIST_READONLY_CONSUMERS.matches(methodInvocation);
    }
    return isListConstructor(tree);
  }

  private static boolean isListConstructor(@Nullable Tree tree) {
    return tree instanceof NewClassTree newClassTree && LIST_CONSTRUCTORS.matches(newClassTree);
  }

  private static Tree findParentScope(@Nullable Tree tree, Set supportedChildKinds) {
    while (tree != null && supportedChildKinds.contains(tree.kind())) {
      tree = tree.parent();
    }
    return tree;
  }

  /**
   * @return true if we can reach "reverseParentScope" following "tree" parents and continuing only for
   * the given Tree.Kind list. This will ensure that between the given tree and the "Collections.reverse(list)" call,
   * there are no loops or lambdas.
   */
  private static boolean matchReverseParentSafeScope(@Nullable Tree tree, Tree reverseParentScope) {
    return findParentScope(tree, SUPPORTED_USAGE_SCOPE_CHILD_KINDS) == reverseParentScope;
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy