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

org.sonar.java.checks.PrintfCheck Maven / Gradle / Ivy

There is a newer version: 8.6.0.37351
Show newest version
/*
 * SonarQube Java
 * Copyright (C) 2012-2016 SonarSource SA
 * mailto:contact AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * 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 GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package org.sonar.java.checks;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.sonar.api.server.rule.RulesDefinition;
import org.sonar.check.Priority;
import org.sonar.check.Rule;
import org.sonar.java.checks.methods.AbstractMethodDetection;
import org.sonar.java.matcher.MethodMatcher;
import org.sonar.java.model.LiteralUtils;
import org.sonar.java.syntaxtoken.FirstSyntaxTokenFinder;
import org.sonar.java.tag.Tag;
import org.sonar.plugins.java.api.semantic.Type;
import org.sonar.plugins.java.api.tree.BinaryExpressionTree;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.LiteralTree;
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.squidbridge.annotations.ActivatedByDefault;
import org.sonar.squidbridge.annotations.SqaleConstantRemediation;
import org.sonar.squidbridge.annotations.SqaleSubCharacteristic;

import javax.annotation.Nullable;

import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Rule(
  key = "S2275",
  name = "Printf-style format strings should not lead to unexpected behavior at runtime",
  priority = Priority.CRITICAL,
  tags = {Tag.BUG, Tag.PITFALL})
@ActivatedByDefault
@SqaleSubCharacteristic(RulesDefinition.SubCharacteristics.INSTRUCTION_RELIABILITY)
@SqaleConstantRemediation("10min")
public class PrintfCheck extends AbstractMethodDetection {

  private static final Pattern PRINTF_PARAM_PATTERN = Pattern.compile("%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])");
  private static final Set TIME_CONVERSIONS = Sets.newHashSet(
    "H", "I", "k", "l", "M", "S", "L", "N", "p", "z", "Z", "s", "Q",
    "B", "b", "h", "A", "a", "C", "Y", "y", "j", "m", "d", "e",
    "R", "T", "r", "D", "F", "c"
    );
  private static final String FORMAT_METHOD_NAME = "format";

  @Override
  protected List getMethodInvocationMatchers() {
    return ImmutableList.of(
      MethodMatcher.create().typeDefinition("java.lang.String").name(FORMAT_METHOD_NAME).withNoParameterConstraint(),
      MethodMatcher.create().typeDefinition("java.util.Formatter").name(FORMAT_METHOD_NAME).withNoParameterConstraint(),
      MethodMatcher.create().typeDefinition("java.io.PrintStream").name(FORMAT_METHOD_NAME).withNoParameterConstraint(),
      MethodMatcher.create().typeDefinition("java.io.PrintStream").name("printf").withNoParameterConstraint(),
      MethodMatcher.create().typeDefinition("java.io.PrintWriter").name(FORMAT_METHOD_NAME).withNoParameterConstraint(),
      MethodMatcher.create().typeDefinition("java.io.PrintWriter").name("printf").withNoParameterConstraint()
      );
  }

  @Override
  protected void onMethodInvocationFound(MethodInvocationTree mit) {
    ExpressionTree formatStringTree;
    List args;
    // Check type of first argument:
    if (mit.arguments().get(0).symbolType().is("java.lang.String")) {
      formatStringTree = mit.arguments().get(0);
      args = mit.arguments().subList(1, mit.arguments().size());
    } else {
      // format method with "Locale" first argument, skip that one.
      formatStringTree = mit.arguments().get(1);
      args = mit.arguments().subList(2, mit.arguments().size());
    }
    if (formatStringTree.is(Tree.Kind.STRING_LITERAL)) {
      String formatString = LiteralUtils.trimQuotes(((LiteralTree) formatStringTree).value());
      checkLineFeed(formatString, mit);

      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;
      }
      if (params.isEmpty()) {
        reportIssue(mit, "String contains no format specifiers.");
        return;
      }
      cleanupLineSeparator(params);
      if (argIndexes(params).size() > args.size()) {
        reportIssue(mit, "Not enough arguments.");
        return;
      }
      verifyParameters(mit, args, params);
    } else if (isConcatenationOnSameLine(formatStringTree)) {
      reportIssue(mit, "Format specifiers should be used instead of string concatenation.");
    }
  }

  private static Set argIndexes(List params) {
    int index = 0;
    Set result = Sets.newHashSet();
    for (String rawParam : params) {
      if (rawParam.contains("$")) {
        result.add(getIndex(rawParam));
      } else if (rawParam.charAt(0) != '<') {
        index++;
        result.add(index);
      }
    }
    return result;
  }

  private static boolean isConcatenationOnSameLine(ExpressionTree formatStringTree) {
    return formatStringTree.is(Tree.Kind.PLUS) && operandsAreOnSameLine((BinaryExpressionTree) formatStringTree);
  }

  private static boolean operandsAreOnSameLine(BinaryExpressionTree formatStringTree) {
    return FirstSyntaxTokenFinder.firstSyntaxToken(formatStringTree.leftOperand()).line() == FirstSyntaxTokenFinder.firstSyntaxToken(formatStringTree.rightOperand()).line();
  }

  private static void cleanupLineSeparator(List params) {
    // Cleanup %n and %% values
    Iterator iter = params.iterator();
    while (iter.hasNext()) {
      String param = iter.next();
      if ("n".equals(param) || "%".equals(param)) {
        iter.remove();
      }
    }
  }

  private void checkLineFeed(String formatString, MethodInvocationTree mit) {
    if (formatString.contains("\\n")) {
      reportIssue(mit, "%n should be used in place of \\n to produce the platform-specific line separator.");
    }
  }

  private void verifyParameters(MethodInvocationTree mit, List args, List params) {
    int index = 0;
    List unusedArgs = Lists.newArrayList(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++;
      }
      ExpressionTree argExpressionTree = args.get(argIndex);
      unusedArgs.remove(argExpressionTree);
      Type argType = argExpressionTree.symbolType();
      checkNumerical(mit, param, argType);
      checkBoolean(mit, param, argType);
      checkTimeConversion(mit, param, argType);

    }
    reportUnusedArgs(mit, args, unusedArgs);
  }

  private static Integer getIndex(String param) {
    return Integer.valueOf(param.substring(0, param.indexOf('$')));
  }

  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 checkNumerical(MethodInvocationTree mit, String param, Type argType) {
    if (param.charAt(0) == 'd' && !isNumerical(argType)) {
      reportIssue(mit, "An 'int' is expected rather than a " + argType + ".");
    }
  }

  private void checkTimeConversion(MethodInvocationTree mit, String param, Type argType) {
    if (param.charAt(0) == 't' || param.charAt(0) == 'T') {
      String timeConversion = param.substring(1);
      if (timeConversion.isEmpty()) {
        reportIssue(mit, "Time conversion requires a second character.");
        checkTimeTypeArgument(mit, argType);
        return;
      }
      if (!TIME_CONVERSIONS.contains(timeConversion)) {
        reportIssue(mit, timeConversion + " is not a supported time conversion character");
      }
      checkTimeTypeArgument(mit, argType);
    }
  }

  private void checkTimeTypeArgument(MethodInvocationTree mit, Type argType) {
    if (!(argType.isNumerical() || argType.is("java.lang.Long") || argType.isSubtypeOf("java.util.Date") || argType.isSubtypeOf("java.util.Calendar"))) {
      reportIssue(mit, "Time argument is expected (long, Long, Date or Calendar).");
    }
  }

  private static boolean isNumerical(Type argType) {
    return argType.isNumerical()
      || isOneOf(argType,
        "java.math.BigInteger",
        "java.math.BigDecimal",
        "java.lang.Byte",
        "java.lang.Short",
        "java.lang.Integer",
        "java.lang.Long",
        "java.lang.Float",
        "java.lang.Double");
  }

  private static boolean isOneOf(Type argType, String... fullyQualifiedNames) {
    for (String fullyQualifiedName : fullyQualifiedNames) {
      if (argType.is(fullyQualifiedName)) {
        return true;
      }
    }
    return false;
  }

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

  private void reportUnusedArgs(MethodInvocationTree mit, List args, List unusedArgs) {
    for (ExpressionTree unusedArg : unusedArgs) {
      int i = args.indexOf(unusedArg);
      String stringArgIndex = "first";
      if (i == 1) {
        stringArgIndex = "2nd";
      } else if (i == 2) {
        stringArgIndex = "3rd";
      } else if (i >= 3) {
        stringArgIndex = (i + 1) + "th";
      }
      reportIssue(mit, stringArgIndex + " argument is not used.");
    }
  }

  private List getParameters(String formatString, MethodInvocationTree mit) {
    List params = Lists.newArrayList();
    Matcher matcher = PRINTF_PARAM_PATTERN.matcher(formatString);
    while (matcher.find()) {
      if (firstArgumentIsLT(params, matcher.group(2))) {
        reportIssue(mit, "The argument index '<' refers to the previous format specifier but there isn't one.");
        continue;
      }
      StringBuilder param = new StringBuilder();
      for (int groupIndex : new int[] {1, 2, 5, 6}) {
        if (matcher.group(groupIndex) != null) {
          param.append(matcher.group(groupIndex));
        }
      }
      params.add(param.toString());
    }
    return params;
  }

  private static boolean firstArgumentIsLT(List params, @Nullable String group) {
    return params.isEmpty() && group != null && group.length() > 0 && group.charAt(0) == '<';
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy