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

org.sonar.java.checks.PrintfMisuseCheck 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.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
import org.sonar.check.Rule;
import org.sonar.java.model.ExpressionUtils;
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;
import org.sonar.plugins.java.api.tree.TypeTree;
import org.sonar.plugins.java.api.tree.UnionTypeTree;
import org.sonar.plugins.java.api.tree.VariableTree;

import static org.sonar.plugins.java.api.semantic.MethodMatchers.ANY;

@Rule(key = "S3457")
public class PrintfMisuseCheck extends AbstractPrintfChecker {

  private static final String ORG_SLF4J_LOGGER = "org.slf4j.Logger";
  private static final String JAVA_UTIL_LOGGING_LOGGER = "java.util.logging.Logger";

  private static final MethodMatchers TO_STRING = MethodMatchers.create()
    .ofAnyType().names("toString").addWithoutParametersMatcher().build();
  private static final MethodMatchers GET_LOGGER = MethodMatchers.or(
    MethodMatchers.create()
      .ofTypes(JAVA_UTIL_LOGGING_LOGGER).names("getLogger").addParametersMatcher(JAVA_LANG_STRING, JAVA_LANG_STRING).build(),
    MethodMatchers.create()
      .ofTypes(JAVA_UTIL_LOGGING_LOGGER).names("getAnonymousLogger").addParametersMatcher(JAVA_LANG_STRING).build());

  private static final MethodMatchers JAVA_UTIL_LOGGER_LOG_LEVEL_STRING = MethodMatchers.create()
    .ofTypes(JAVA_UTIL_LOGGING_LOGGER)
    .names("log")
    .addParametersMatcher("java.util.logging.Level", JAVA_LANG_STRING)
    .build();
  private static final MethodMatchers JAVA_UTIL_LOGGER_LOG_LEVEL_STRING_ANY = MethodMatchers.create()
    .ofTypes(JAVA_UTIL_LOGGING_LOGGER)
    .names("log")
    .addParametersMatcher("java.util.logging.Level", JAVA_LANG_STRING, ANY)
    .build();
  private static final MethodMatchers JAVA_UTIL_LOGGER_LOG_MATCHER = MethodMatchers.or(
    JAVA_UTIL_LOGGER_LOG_LEVEL_STRING,
    JAVA_UTIL_LOGGER_LOG_LEVEL_STRING_ANY);

  private static final MethodMatchers SLF4J_METHOD_MATCHERS = MethodMatchers.or(LEVELS.stream()
    .map(l -> MethodMatchers.create().ofTypes(ORG_SLF4J_LOGGER).names(l).withAnyParameters().build())
    .toList());

  @Override
  protected MethodMatchers getMethodInvocationMatchers() {
    ArrayList matchers = new ArrayList<>();
    matchers.add(SLF4J_METHOD_MATCHERS);
    matchers.add(super.getMethodInvocationMatchers());
    // Add log methods as they only apply to misuse and not error.
    matchers.add(log4jMethods());
    matchers.add(JAVA_UTIL_LOGGER_LOG_LEVEL_STRING);
    matchers.add(JAVA_UTIL_LOGGER_LOG_LEVEL_STRING_ANY);
    return MethodMatchers.or(matchers);
  }

  private static MethodMatchers log4jMethods() {
    List methodNames = new ArrayList<>();
    methodNames.add(PRINTF_METHOD_NAME);
    methodNames.add("log");
    methodNames.addAll(LEVELS);
    return MethodMatchers.create()
      .ofTypes(ORG_APACHE_LOGGING_LOG4J_LOGGER)
      .names(methodNames.toArray(new String[0]))
      .withAnyParameters()
      .build();
  }

  @Override
  protected void onMethodInvocationFound(MethodInvocationTree mit) {
    boolean isMessageFormat = MESSAGE_FORMAT.matches(mit);
    if (isMessageFormat && !mit.methodSymbol().isStatic()) {
      // only consider the static method
      return;
    }
    if (!isMessageFormat && JAVA_UTIL_LOGGER_LOG_MATCHER.matches(mit) && hasResourceBundle(mit)) {
      return;
    }
    if (!isMessageFormat) {
      isMessageFormat = JAVA_UTIL_LOGGER_LOG_LEVEL_STRING_ANY.matches(mit);
    }
    if (!isMessageFormat) {
      isMessageFormat = isLoggingMethod(mit);
    }
    super.checkFormatting(mit, isMessageFormat);
  }

  private static boolean hasResourceBundle(MethodInvocationTree mit) {
    Tree id;
    if (mit.methodSelect().is(Tree.Kind.MEMBER_SELECT)) {
      id = ((MemberSelectExpressionTree) mit.methodSelect()).expression();
    } else {
      // defensive programming : cannot be reached : log methods are not static
      return false;
    }
    if (id.is(Tree.Kind.MEMBER_SELECT)) {
      id = ((MemberSelectExpressionTree) id).identifier();
    }
    if (id.is(Tree.Kind.IDENTIFIER)) {
      Tree decl = ((IdentifierTree) id).symbol().declaration();
      if (decl != null && decl.is(Tree.Kind.VARIABLE)) {
        VariableTree variable = ((VariableTree) decl);
        ExpressionTree initializer = variable.initializer();
        if (initializer != null && initializer.is(Tree.Kind.METHOD_INVOCATION)) {
          return GET_LOGGER.matches((MethodInvocationTree) initializer);
        }
      }
    }
    return false;
  }

  @Override
  protected void handlePrintfFormat(MethodInvocationTree mit, String formatString, List args) {
    handlePrintfFormat(mit, formatString, args, false);
  }

  @Override
  protected void handlePrintfFormatCatchingErrors(MethodInvocationTree mit, String formatString, List args) {
    handlePrintfFormat(mit, formatString, args, true);
  }

  private void handlePrintfFormat(MethodInvocationTree mit, String formatString, List args, boolean catchErrors) {
    List params = getParameters(formatString, mit);
    if (usesMessageFormat(formatString, params)) {
      reportIssue(mit, "Looks like there is a confusion with the use of java.text.MessageFormat, parameters will be simply ignored here");
      return;
    }
    checkLineFeed(formatString, mit);
    if (params.isEmpty() && (!args.isEmpty() || !isLoggingMethod(mit))) {
      reportIssue(mit, "String contains no format specifiers.");
      return;
    }
    cleanupLineSeparator(params);
    if (!params.isEmpty()) {
      if (argIndexes(params).size() <= args.size()) {
        verifyParametersForMisuse(mit, args, params);
      }
      if (catchErrors) {
        // Errors are caught, we can report them in this rule
        if (checkArgumentNumber(mit, argIndexes(params).size(), args.size())) {
          return;
        }
        verifyParametersForErrors(mit, args, params);
      }
    }
  }

  private void verifyParametersForMisuse(MethodInvocationTree mit, List args, List params) {
    int index = 0;
    List unusedArgs = new ArrayList<>(args);
    for (String rawParam : params) {
      String param = rawParam;
      int argIndex = index;
      if (param.contains("$")) {
        argIndex = getIndex(param) - 1;
        if (argIndex == -1) {
          reportIssue(mit, "Arguments are numbered starting from 1.");
          return;
        }
        param = param.substring(param.indexOf('$') + 1);
      } else if (param.charAt(0) == '<') {
        // refers to previous argument
        argIndex = Math.max(0, argIndex - 1);
      } else {
        index++;
      }
      if (argIndex >= args.size()) {
        // indexes are obviously wrong - will be caught by S2275 (PrintfFailCheck)
        return;
      }
      ExpressionTree argExpressionTree = args.get(argIndex);
      unusedArgs.remove(argExpressionTree);
      Type argType = argExpressionTree.symbolType();
      checkBoolean(mit, param, argType);
    }
    reportUnusedArgs(mit, args, unusedArgs);
  }

  @Override
  protected void handleMessageFormat(MethodInvocationTree mit, String formatString, List args) {
    String newFormatString = cleanupDoubleQuote(formatString);
    Set indexes = getMessageFormatIndexes(newFormatString, mit);
    List transposedArgs = transposeArgumentArrayAndRemoveThrowable(mit, args, indexes);
    if (transposedArgs == null) {
      return;
    }
    if (indexes.isEmpty() && !transposedArgs.isEmpty()) {
      reportIssue(mit, "String contains no format specifiers.");
      return;
    }
    if (checkArgumentNumber(mit, indexes.size(), transposedArgs.size())
      || checkUnbalancedQuotes(mit, newFormatString)) {
      return;
    }
    checkToStringInvocation(transposedArgs);
    verifyParameters(mit, transposedArgs, indexes);
  }

  private boolean checkUnbalancedQuotes(MethodInvocationTree mit, String formatString) {
    if (LEVELS.contains(mit.methodSymbol().name())) {
      return false;
    }

    String withoutParam = MESSAGE_FORMAT_PATTERN.matcher(formatString).replaceAll("");
    int numberQuote = 0;
    for (int i = 0; i < withoutParam.length(); ++i) {
      if (withoutParam.charAt(i) == '\'') {
        numberQuote++;
      }
    }

    boolean unbalancedQuotes = (numberQuote % 2) != 0;

    if (unbalancedQuotes && MESSAGE_FORMAT_PATTERN_PREDICATE.test(formatString)) {
      // Single quotes should be escaped only when unbalanced and in MessageFormat pattern.
      reportIssue(mit.arguments().get(0), "Single quote \"'\" must be escaped.");
    }

    return unbalancedQuotes;
  }

  @Nullable
  private static List transposeArgumentArrayAndRemoveThrowable(MethodInvocationTree mit, List args, Set indexes) {
    return transposeArgumentArray(args).map(transposedArgs -> {
      if (lastArgumentShouldBeIgnored(mit, args, transposedArgs, indexes)) {
        return transposedArgs.subList(0, transposedArgs.size() - 1);
      } else {
        return transposedArgs;
      }
    }).orElse(null);
  }

  private static boolean lastArgumentShouldBeIgnored(MethodInvocationTree mit, List args, List transposedArgs, Set indexes) {
    if (!isLoggingMethod(mit)) {
      return false;
    }
    if (mit.methodSymbol().owner().type().is(JAVA_UTIL_LOGGING_LOGGER)) {
      // Remove the last argument from the count if it's a throwable, since log(Level level, String msg, Throwable thrown) will be called.
      // If the argument is an array, any exception in the array will be considered as Object, behaving as any others.
      return args.size() == 1 && isLastArgumentThrowable(args);
    }
    // org.apache.logging.log4j.Logger and org.slf4j.Logger
    if (transposedArgs.size() == 1) {
      // Logging methods with only one throwable argument will treat it differently (and should be removed from the count).
      return isLastArgumentThrowable(transposedArgs);
    } else {
      // One extra throwable argument can be consumed by logging methods, it should be removed from the count if it exists.
      return (transposedArgs.size() > indexes.size()) && isLastArgumentThrowable(transposedArgs);
    }
  }

  private static boolean isLastArgumentThrowable(List arguments) {
    if (arguments.isEmpty()) {
      return false;
    }
    ExpressionTree lastArgument = arguments.get(arguments.size() - 1);
    if (lastArgument.symbolType().isSubtypeOf(JAVA_LANG_THROWABLE)) {
      return true;
    }
    return hasUnknownExceptionInUnionType(ExpressionUtils.skipParentheses(lastArgument));
  }

  /**
   * Limitation of ECJ, which will approximate type of a variable to 'Object' if some types
   * are unknown in its defining union type, for instance: in the following catch tree:
   *
   * catch (UnknownException | IllegalArgumentException e) { ... }
   *
   * leads to have 'e' being of type 'Object'
   */
  private static boolean hasUnknownExceptionInUnionType(ExpressionTree lastArgument) {
    if (!lastArgument.is(Tree.Kind.IDENTIFIER)) {
      return false;
    }
    Symbol symbol = ((IdentifierTree) lastArgument).symbol();
    VariableTree declaration = symbol.isVariableSymbol() ? ((Symbol.VariableSymbol) symbol).declaration() : null;
    if (declaration == null) {
      return false;
    }
    TypeTree declarationType = declaration.type();
    return declarationType.is(Tree.Kind.UNION_TYPE)
      && ((UnionTypeTree) declarationType)
        .typeAlternatives()
        .stream()
        .map(TypeTree::symbolType)
        .anyMatch(Type::isUnknown);
  }

  private void checkToStringInvocation(List args) {
    args.stream()
      .filter(arg -> arg.is(Tree.Kind.METHOD_INVOCATION))
      .map(MethodInvocationTree.class::cast)
      .filter(TO_STRING::matches)
      .filter(arg -> arg != args.get(args.size() - 1) || !isMethodOfThrowable(arg))
      .forEach(arg -> reportIssue(arg, getToStringMessage(arg)));
  }

  private static boolean isMethodOfThrowable(MethodInvocationTree argument) {
    Symbol owner = argument.methodSymbol().owner();
    return owner != null && owner.type().isSubtypeOf(JAVA_LANG_THROWABLE);
  }

  private static String getToStringMessage(ExpressionTree arg) {
    if (isInStringArrayInitializer(arg)) {
      return "No need to call \"toString()\" method since an array of Objects can be used here.";
    }
    return "No need to call \"toString()\" method as formatting and string conversion is done by the Formatter.";
  }

  private static boolean isInStringArrayInitializer(ExpressionTree arg) {
    return Optional.of(arg)
      .map(Tree::parent)
      .filter(tree -> tree.is(Tree.Kind.LIST))
      .map(Tree::parent)
      .filter(tree -> tree.is(Tree.Kind.NEW_ARRAY))
      .map(NewArrayTree.class::cast)
      .map(ExpressionTree::symbolType)
      .filter(Type::isArray)
      .map(Type.ArrayType.class::cast)
      .map(Type.ArrayType::elementType)
      .filter(type -> type.is(JAVA_LANG_STRING))
      .isPresent();
  }

  private void verifyParameters(MethodInvocationTree mit, List args, Set indexes) {
    List unusedArgs = new ArrayList<>(args);
    for (int index : indexes) {
      if (index >= args.size()) {
        reportIssue(mit, "Not enough arguments.");
        return;
      }
      unusedArgs.remove(args.get(index));
    }
    reportUnusedArgs(mit, args, unusedArgs);
  }

  private void reportUnusedArgs(MethodInvocationTree mit, List args, List unusedArgs) {
    for (ExpressionTree unusedArg : unusedArgs) {
      int i = args.indexOf(unusedArg);
      reportIssue(mit, postFixedIndex(i) + " argument is not used.");
    }
  }

  private static String postFixedIndex(int i) {
    if (i < 1) {
      return "first";
    } else if (i < 2) {
      return "2nd";
    } else if (i < 3) {
      return "3rd";
    } else {
      return (i + 1) + "th";
    }
  }

  private void checkBoolean(MethodInvocationTree mit, String param, Type argType) {
    if (param.charAt(0) == 'b' && !(argType.is("boolean") || argType.is("java.lang.Boolean"))) {
      reportIssue(mit, "Directly inject the boolean value.");
    }
  }

  private void checkLineFeed(String formatString, MethodInvocationTree mit) {
    var index = formatString.indexOf("\\n");
    while (index != -1) {
      if (isOddNumberOfEscapeChars(formatString, index)) {
        reportIssue(mit, "%n should be used in place of \\n to produce the platform-specific line separator.");
        return;
      }
      index = formatString.indexOf("\\n", index+2);
    }
  }

  private static boolean isOddNumberOfEscapeChars(String formatString, int lastIndex) {
    var index = lastIndex-1;
    while (index >= 0 && formatString.charAt(index) == '\\') {
      index--;
    }
    return (lastIndex - index) % 2 != 0;
  }

  private static boolean usesMessageFormat(String formatString, List params) {
    return params.isEmpty() && (formatString.contains("{0") || formatString.contains("{1"));
  }

  @Override
  protected void handleOtherFormatTree(MethodInvocationTree mit, ExpressionTree formatTree, List args) {
    if (isIncorrectConcatenation(formatTree)) {
      boolean lastArgumentThrowable = isLastArgumentThrowable(args);
      if (JAVA_UTIL_LOGGER_LOG_MATCHER.matches(mit)) {
        if (lastArgumentThrowable) {
          reportIssue(mit, "Lambda should be used to defer string concatenation.");
        } else {
          reportIssue(mit, "Format specifiers or lambda should be used instead of string concatenation.");
        }
      } else if (!(lastArgumentThrowable && SLF4J_METHOD_MATCHERS.matches(mit))) {
        reportIssue(mit, "Format specifiers should be used instead of string concatenation.");
      }
    }
  }

  private static boolean isIncorrectConcatenation(ExpressionTree formatStringTree) {
    return formatStringTree.is(Tree.Kind.PLUS) && !formatStringTree.asConstant().isPresent();
  }

  private static boolean isLoggingMethod(MethodInvocationTree mit) {
    String methodName = mit.methodSymbol().name();
    return "log".equals(methodName) || LEVELS.contains(methodName);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy