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

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

import java.util.List;
import java.util.Optional;
import org.sonar.check.Rule;
import org.sonar.java.checks.methods.AbstractMethodDetection;
import org.sonar.plugins.java.api.JavaFileScannerContext;
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.tree.BinaryExpressionTree;
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.Tree;


@Rule(key = "S6915")
public class StringIndexOfRangesCheck extends AbstractMethodDetection implements JavaVersionAwareVisitor {

  private static final String JAVA_LANG_STRING = "java.lang.String";

  private static final String BEGIN_IDX_SMALLER_THAN_STR_LENGTH = "Begin index should be smaller than the length of the string.";
  private static final String END_IDX_AT_MOST_STR_LENGTH = "End index should be at most the length of the string.";
  private static final String BEGIN_IDX_NOT_LARGER_THAN_END_IDX = "Begin index should not be larger than endIndex.";
  private static final String END_INDEX = "End index";
  private static final String RECEIVER_STRING = "Receiver string";

  private static final MethodMatchers INDEX_OF_MATCHERS =
    MethodMatchers.create()
      .ofTypes(JAVA_LANG_STRING)
      .names("indexOf")
      .addParametersMatcher("int", "int", "int")
      .addParametersMatcher(JAVA_LANG_STRING, "int", "int")
      .build();

  private static final MethodMatchers LENGTH_MATCHERS =
    MethodMatchers.create()
      .ofTypes(JAVA_LANG_STRING)
      .names("length")
      .addWithoutParametersMatcher()
      .build();

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

  @Override
  protected MethodMatchers getMethodInvocationMatchers() {
    return INDEX_OF_MATCHERS;
  }

  @Override
  protected void onMethodInvocationFound(MethodInvocationTree methodInvocation) {
    var issueFound = checkConstantBounds(methodInvocation);
    if (!issueFound && methodInvocation.methodSelect() instanceof MemberSelectExpressionTree memberSelect
      && memberSelect.expression() instanceof IdentifierTree idTree) {
      checkBoundsThatDependOnLength(methodInvocation, idTree.name());
    }
  }

  /**
   * Return value indicates whether an issue has been found or not
   */
  private boolean checkConstantBounds(MethodInvocationTree methodInvocation) {
    var beginIdxExpr = methodInvocation.arguments().get(1);
    var endIdxExpr = methodInvocation.arguments().get(2);

    var beginIdxConst = beginIdxExpr.asConstant(Integer.class);
    var endIdxConst = endIdxExpr.asConstant(Integer.class);

    if (beginIdxConst.isPresent() && beginIdxConst.get() < 0) {
      reportIssue(beginIdxExpr, "Begin index should be non-negative.");
      return true;
    }

    if (beginIdxConst.isPresent() && endIdxConst.isPresent() && beginIdxConst.get() > endIdxConst.get()) {
      reportWithSecondaryLocation(beginIdxExpr, BEGIN_IDX_NOT_LARGER_THAN_END_IDX, endIdxExpr, END_INDEX);
      return true;
    }

    Optional receiverConst = methodInvocation.methodSelect() instanceof MemberSelectExpressionTree memberSelect
      ? memberSelect.expression().asConstant(String.class) : Optional.empty();

    if (receiverConst.isPresent() && beginIdxConst.isPresent() && beginIdxConst.get() >= receiverConst.get().length()) {
      reportWithSecondaryLocation(beginIdxExpr, BEGIN_IDX_SMALLER_THAN_STR_LENGTH, methodInvocation.methodSelect(), RECEIVER_STRING);
      return true;
    }

    if (receiverConst.isPresent() && endIdxConst.isPresent() && endIdxConst.get() > receiverConst.get().length()) {
      reportWithSecondaryLocation(endIdxExpr, END_IDX_AT_MOST_STR_LENGTH, methodInvocation.methodSelect(), RECEIVER_STRING);
      return true;
    }

    return false;
  }

  private void checkBoundsThatDependOnLength(MethodInvocationTree methodInvocation, String lengthReceiverVarName) {
    var beginIdxExpr = methodInvocation.arguments().get(1);
    var endIdxExpr = methodInvocation.arguments().get(2);
    var beginIdxDelta = lengthDelta(beginIdxExpr, lengthReceiverVarName);
    var endIdxDelta = lengthDelta(endIdxExpr, lengthReceiverVarName);
    if (beginIdxDelta.isPresent() && beginIdxDelta.get() >= 0) {
      reportIssue(beginIdxExpr, BEGIN_IDX_SMALLER_THAN_STR_LENGTH);
    } else if (endIdxDelta.isPresent() && endIdxDelta.get() > 0) {
      reportIssue(endIdxExpr, END_IDX_AT_MOST_STR_LENGTH);
    } else if (beginIdxDelta.isPresent() && endIdxDelta.isPresent() && beginIdxDelta.get() > endIdxDelta.get()) {
      reportWithSecondaryLocation(beginIdxExpr, BEGIN_IDX_NOT_LARGER_THAN_END_IDX, endIdxExpr, END_INDEX);
    }
  }

  private void reportWithSecondaryLocation(ExpressionTree tree, String msg, ExpressionTree secondaryLocation, String secondaryMsg) {
    reportIssue(tree, msg, List.of(new JavaFileScannerContext.Location(secondaryMsg, secondaryLocation)), null);
  }

  /**
   * @param expr    the expression
   * @param varName name of the variable
   * @return an Optional containing the difference delta such that the value of expr is var.length() + delta (delta may be negative),
   * or an empty Optional if the expression is too complex for delta to be computed
   */
  private static Optional lengthDelta(ExpressionTree expr, String varName) {
    if (isCallToLengthOnVariable(expr, varName)) {
      // 1st pattern: var.length()
      return Optional.of(0);
    }
    if (expr instanceof BinaryExpressionTree binaryExpr) {
      var isPlus = binaryExpr.kind() == Tree.Kind.PLUS;
      var isMinus = binaryExpr.kind() == Tree.Kind.MINUS;
      if (!(isPlus || isMinus)) {
        return Optional.empty();
      }
      var leftCst = binaryExpr.leftOperand().asConstant(Integer.class);
      var rightCst = binaryExpr.rightOperand().asConstant(Integer.class);
      if (isCallToLengthOnVariable(binaryExpr.leftOperand(), varName) && rightCst.isPresent()) {
        // 2nd pattern:  var.length() + cst  or  var.length() - cst
        return isPlus ? rightCst : rightCst.map(x -> -x);
      } else if (isPlus && leftCst.isPresent() && isCallToLengthOnVariable(binaryExpr.rightOperand(), varName)) {
        // 3rd pattern: cst + var.length()
        return leftCst;
      }
    }
    return Optional.empty();
  }

  private static boolean isCallToLengthOnVariable(ExpressionTree expr, String varName) {
    if (expr instanceof MethodInvocationTree mit) {
      return LENGTH_MATCHERS.matches(mit.methodSymbol())
        && mit.methodSelect() instanceof MemberSelectExpressionTree memberSelect
        && memberSelect.expression() instanceof IdentifierTree idTree
        && idTree.symbol().name().equals(varName);
    }
    return false;
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy