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

org.sonar.python.checks.StringFormatMisuseCheck 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.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.sonar.check.Rule;
import org.sonar.plugins.python.api.SubscriptionContext;
import org.sonar.plugins.python.api.tree.BinaryExpression;
import org.sonar.plugins.python.api.tree.CallExpression;
import org.sonar.plugins.python.api.tree.DictionaryLiteral;
import org.sonar.plugins.python.api.tree.Expression;
import org.sonar.plugins.python.api.tree.RegularArgument;
import org.sonar.plugins.python.api.tree.StringLiteral;
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.plugins.python.api.tree.Tuple;
import org.sonar.plugins.python.api.types.BuiltinTypes;

@Rule(key = "S2275")
public class StringFormatMisuseCheck extends AbstractStringFormatCheck {

  @Override
  public void initialize(Context context) {
    context.registerSyntaxNodeConsumer(Tree.Kind.MODULO, this::checkPrintfStyle);
    context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, this::checkStrFormatStyle);
  }

  @Override
  protected void checkPrintfStyle(SubscriptionContext ctx, BinaryExpression modulo, StringLiteral literal) {
    Optional formatOptional = StringFormat.createFromPrintfStyle(
      syntaxIssueReporter(ctx, modulo.leftOperand(), literal), literal.trimmedQuotesValue());
    if (!formatOptional.isPresent()) {
      // The string format contains invalid syntax.
      return;
    }

    StringFormat format = formatOptional.get();
    Expression rhs = modulo.rightOperand();
    if (format.numExpectedArguments() == 0) {
      // The format does not contain any replacement fields, but with a mapping or a list as RHS, it won't result in a runtime error.
      if (!isMapping(rhs) && !rhs.type().canOnlyBe(BuiltinTypes.LIST)) {
        reportIssue(ctx, modulo.leftOperand(), literal, "Add replacement field(s) to this formatted string.");
      }
      return;
    }

    if (format.hasNamedFields()) {
      checkNamed(ctx, format, rhs);
    } else {
      checkPositional(ctx, format, rhs);
    }
  }

  @Override
  protected void checkStrFormatStyle(SubscriptionContext ctx, CallExpression callExpression, Expression qualifier, StringLiteral literal) {
    // Check the arguments for out of scope cases before we try to parse the string
    if (callExpression.arguments().stream().anyMatch(argument -> !argument.is(Tree.Kind.REGULAR_ARGUMENT))) {
      return;
    }

    Optional format = StringFormat.createFromStrFormatStyle(syntaxIssueReporter(ctx, qualifier, literal), literal.trimmedQuotesValue());
    if (!format.isPresent()) {
      return;
    }

    List arguments = callExpression.arguments().stream()
      .map(RegularArgument.class::cast)
      .toList();

    OptionalInt firstKwIdx = IntStream.range(0, arguments.size())
      .filter(idx -> arguments.get(idx).keywordArgument() != null)
      .findFirst();

    // Check the keyword arguments - build a set of all provided keyword arguments and check if all named fields have
    // a match in this set.
    Set kwArguments = new HashSet<>();
    if (firstKwIdx.isPresent()) {
      arguments.subList(firstKwIdx.getAsInt(), arguments.size()).forEach(argument -> kwArguments.add(argument.keywordArgument().name()));
    }
    format.get().replacementFields().stream()
      .filter(field -> field.isNamed() && !kwArguments.contains(field.name()))
      .forEach(field -> reportIssue(ctx, qualifier, literal, String.format("Provide a value for field \"%s\".", field.name())));

    // Produce a list of unmatched positional indices and re-use it for the issue message.
    // We basically want to see if there is a position in the field list that is larger than the number of
    // the positional arguments provided.
    int firstIdx = firstKwIdx.orElse(arguments.size());
    String unmatchedPositionals = format.get().replacementFields().stream()
      .filter(field -> field.isPositional() && field.position() >= firstIdx)
      .map(field -> String.valueOf(field.position()))
      .distinct()
      .collect(Collectors.joining(", "));

    if (!unmatchedPositionals.isEmpty()) {
      reportIssue(ctx, qualifier, literal, String.format("Provide a value for field(s) with index %s.", unmatchedPositionals));
    }
  }

  private static void checkNamed(SubscriptionContext ctx, StringFormat format, Expression rhs) {
    if (rhs.is(Tree.Kind.DICTIONARY_LITERAL)) {
      checkPrintfDictionary(ctx, format, ((DictionaryLiteral) rhs));
    } else if (!isMapping(rhs)) {
      ctx.addIssue(rhs, "Replace this formatting argument with a mapping.");
    }
  }

  private static void checkPositional(SubscriptionContext ctx, StringFormat format, Expression rhs) {
    if (rhs.is(Tree.Kind.TUPLE)) {
      checkTuples(ctx, format, ((Tuple) rhs));
    } else if (format.numExpectedArguments() == 1) {
      format.replacementFields().get(0).validateArgument(ctx, rhs);
    } else if (!rhs.type().canBeOrExtend("tuple")) {
      // Positional fields require tuples
      ctx.addIssue(rhs, "Replace this formatting argument with a tuple.");
    }
  }

  private static void checkTuples(SubscriptionContext ctx, StringFormat format, Tuple tuple) {
    if (tuple.elements().stream().anyMatch(expression -> expression.is(Tree.Kind.UNPACKING_EXPR))) {
      return;
    }

    checkPrintfExpressionList(ctx, format, tuple.firstToken(), tuple.lastToken(), tuple.elements());
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy