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

org.sonar.python.checks.UseStartsWithEndsWithCheck Maven / Gradle / Ivy

There is a newer version: 4.23.0.17664
Show newest version
/*
 * SonarQube Python Plugin
 * Copyright (C) 2011-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.python.checks;

import java.util.Map;
import javax.annotation.CheckForNull;
import org.sonar.check.Rule;
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
import org.sonar.plugins.python.api.SubscriptionContext;
import org.sonar.plugins.python.api.tree.BinaryExpression;
import org.sonar.plugins.python.api.tree.Expression;
import org.sonar.plugins.python.api.tree.NumericLiteral;
import org.sonar.plugins.python.api.tree.SliceExpression;
import org.sonar.plugins.python.api.tree.SliceItem;
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.plugins.python.api.types.BuiltinTypes;

import static org.sonar.plugins.python.api.tree.Tree.Kind.COMPARISON;
import static org.sonar.plugins.python.api.tree.Tree.Kind.SLICE_ITEM;

@Rule(key = "S6659")
public class UseStartsWithEndsWithCheck extends PythonSubscriptionCheck {
  private static final String USE_STARTSWITH_MESSAGE = "Use `startswith` here.";
  private static final String USE_NOT_STARTSWITH_MESSAGE = "Use `not` and `startswith` here.";
  private static final String USE_ENDSWITH_MESSAGE = "Use `endswith` here.";
  private static final String USE_NOT_ENDSWITH_MESSAGE = "Use `not` and `endswith` here.";
  private static final Map> MESSAGES = Map.of(
    SliceType.PREFIX, Map.of(
      OperatorType.EQUALS, USE_STARTSWITH_MESSAGE,
      OperatorType.NOT_EQUALS, USE_NOT_STARTSWITH_MESSAGE),
    SliceType.SUFFIX, Map.of(
      OperatorType.EQUALS, USE_ENDSWITH_MESSAGE,
      OperatorType.NOT_EQUALS, USE_NOT_ENDSWITH_MESSAGE));

  @Override
  public void initialize(Context context) {
    context.registerSyntaxNodeConsumer(COMPARISON, ctx -> checkComparison(ctx, ((BinaryExpression) ctx.syntaxNode())));
  }

  private static void checkComparison(SubscriptionContext ctx, BinaryExpression comparison) {
    var operatorType = OperatorType.fromString(comparison.operator().value());
    if (operatorType == OperatorType.OTHER) {
      return;
    }

    // Either the left or the right operand must be a slice expression.
    // The other one must be the string we compare it to:
    var lhs = comparison.leftOperand();
    var rhs = comparison.rightOperand();
    final SliceExpression sliceExpression;
    final Expression stringExpression;
    if (lhs.is(Tree.Kind.SLICE_EXPR)) {
      sliceExpression = (SliceExpression) lhs;
      stringExpression = rhs;
    } else if (rhs.is(Tree.Kind.SLICE_EXPR)) {
      sliceExpression = (SliceExpression) rhs;
      stringExpression = lhs;
    } else {
      return;
    }

    // To avoid FPs, either the slice expression must slice a string, or the object we compare it to must clearly be a string.
    if (!stringExpression.type().mustBeOrExtend(BuiltinTypes.STR) &&
      !sliceExpression.object().type().mustBeOrExtend(BuiltinTypes.STR)) {
      return;
    }

    var slices = sliceExpression.sliceList().slices();
    if (slices.size() != 1) {
      return;
    }

    var sliceItem = slices.get(0);
    if (!sliceItem.is(SLICE_ITEM)) {
      return;
    }

    var sliceType = SliceType.fromSliceItem((SliceItem) sliceItem);

    var message = selectMessage(sliceType, operatorType);
    if (message == null) {
      return;
    }

    ctx.addIssue(comparison, message);
  }

  @CheckForNull
  private static String selectMessage(SliceType sliceType, OperatorType operatorType) {
    var operatorMap = MESSAGES.get(sliceType);
    if (operatorMap == null) {
      return null;
    }

    return operatorMap.get(operatorType);
  }

  private enum SliceType {
    PREFIX,
    SUFFIX,
    COMPLEX;

    private static SliceType fromSliceItem(SliceItem sliceItem) {
      var stride = sliceItem.stride();
      // If the stride is
      // not absent
      // and not None
      // and not the "1" literal
      // then we don't check the rule.
      if (stride != null &&
        !stride.type().mustBeOrExtend(BuiltinTypes.NONE_TYPE) &&
        !(stride.is(Tree.Kind.NUMERIC_LITERAL) &&
          stride.type().mustBeOrExtend(BuiltinTypes.INT) &&
          ((NumericLiteral) stride).valueAsLong() == 1)) {
        return SliceType.COMPLEX;
      }

      var lowerBound = sliceItem.lowerBound();
      var upperBound = sliceItem.upperBound();

      // Case [x:None:...]
      if (!isEmptyBound(lowerBound) &&
        isEmptyBound(upperBound)) {

        return SliceType.SUFFIX;
      }

      // Case [None:x:...]
      if (!isEmptyBound(upperBound) &&
        isEmptyBound(lowerBound)) {
        return SliceType.PREFIX;
      }

      return SliceType.COMPLEX;
    }

    private static boolean isEmptyBound(@CheckForNull Expression bound) {
      return bound == null || bound.type().mustBeOrExtend(BuiltinTypes.NONE_TYPE);
    }
  }

  private enum OperatorType {
    EQUALS,
    NOT_EQUALS,
    OTHER;

    private static OperatorType fromString(String operator) {
      if ("==".equals(operator)) {
        return EQUALS;
      }

      if ("!=".equals(operator)) {
        return NOT_EQUALS;
      }

      return OTHER;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy