Package com.google.errorprone.bugpatterns

Source Code of com.google.errorprone.bugpatterns.MisusedFormattingLogger

/*
* Copyright 2014 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.errorprone.bugpatterns;

import static com.google.errorprone.BugPattern.Category.JDK;
import static com.google.errorprone.BugPattern.MaturityLevel.MATURE;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.google.errorprone.matchers.Matchers.anyOf;
import static com.google.errorprone.matchers.Matchers.isArrayType;
import static com.google.errorprone.matchers.Matchers.isDescendantOfMethod;
import static com.google.errorprone.matchers.Matchers.isPrimitiveArrayType;
import static com.google.errorprone.matchers.Matchers.isSubtypeOf;
import static com.google.errorprone.matchers.Matchers.methodSelect;

import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.Matchers;
import com.google.errorprone.util.ASTHelpers;

import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCLiteral;
import com.sun.tools.javac.tree.TreeMaker;

import edu.umd.cs.findbugs.formatStringChecker.ExtraFormatArgumentsException;
import edu.umd.cs.findbugs.formatStringChecker.Formatter;
import edu.umd.cs.findbugs.formatStringChecker.FormatterException;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.FormatFlagsConversionMismatchException;
import java.util.HashSet;
import java.util.IllegalFormatException;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

import javax.lang.model.type.TypeKind;

/**
* @author vidarh@google.com (Will Holen)
*/
@BugPattern(name = "MisusedFormattingLogger",
    summary = "FormattingLogger uses wrong or mismatched format string",
    explanation = "FormattingLogger is easily misused. There are several similar but "
        + "incompatible methods.  Methods ending in \"fmt\" use String.format, but the "
        + "corresponding methods without that suffix use MessageFormat. Some methods have an "
        + "optional exception first, and some have it last. Failing to pick "
        + "the right method will cause logging information to be lost or the log call to "
        + "fail at runtime -- often during an error condition when you need it most.\n\n"
        + "There are further gotchas.  For example, MessageFormat strings cannot "
        + "have unbalanced single quotes (e.g., \"Don't log {0}\" will not format {0} because "
        + "of the quote in \"Don't\"). The number of format elements must match the number of "
        + "arguments provided, and for String.format, the types must match as well.  And so on.",
    category = JDK, maturity = MATURE, severity = WARNING)

public class MisusedFormattingLogger extends BugChecker implements MethodInvocationTreeMatcher {

  private static final Matcher<MethodInvocationTree> isFormattingLogger = anyOf(
      methodSelect(Matchers.methodReceiver(
          Matchers.isSubtypeOf("com.google.common.logging.FormattingLogger"))),
      methodSelect(Matchers.methodReceiver(
          Matchers.isSubtypeOf("com.google.gdata.util.common.logging.FormattingLogger"))));

  private static final Matcher<Tree> isThrowable =
      isSubtypeOf("java.lang.Throwable");

  private static final Matcher<MethodInvocationTree> isThrowableMessage =
      methodSelect(Matchers.<ExpressionTree>anyOf(
          isDescendantOfMethod("java.lang.Throwable", "getMessage()"),
          isDescendantOfMethod("java.lang.Throwable", "toString()")));

  /**
   * A regex pattern for matching logging methods in FormattingLogger.
   *
   * <p>This class has a combinatorial number of methods derived from
   * <ol>
   *   <li>A log level name, or generically "log"</li>
   *   <li>A format type ("fmt" for printf, nothing for MessageFormat)</li>
   *   <li>A Level parameter if not specified by the method name</li>
   *   <li>Zero or One Throwable parameter</li>
   *   <li>The format string and parameters</li>
   * </ol>
   *
   * Examples of this includes <code>severe(String, Object...)</code> and
   * <code>infofmt(Throwable, String, Object...)</code>. This pattern matches them all.
   */
  private static final Pattern formattingLoggerMethods = Pattern.compile(
      // Due to Java 1.6 we can't use named groups, so there are constants instead
      "^(severe|warning|info|config|fine|finer|finest|log)(fmt)?\\("
      + "(java\\.util\\.logging\\.Level,)?"
      + "(java\\.lang\\.Throwable,)?"
      + "java\\.lang\\.String,java\\.lang\\.Object\\.\\.\\.\\)");

  private static final int
      BASE_GROUP = 1, FORMAT_GROUP = 2, LEVELPARAM_GROUP = 3, THROWABLEPARAM_GROUP = 4;

  private static final Pattern messageFormatGroup =
      Pattern.compile("\\{ *(\\d+).*?\\}");

  /** A pattern matching a literal region, <strong>after</strong> literal single quote removal. */
  private static final Pattern literalRegion = Pattern.compile("'[^']*'?");

  /** Regex that matches printf groups, copied from
   * edu.umd.cs.findbugs.formatStringChecker.Formatter. */
  private static final Pattern printfGroup =
      Pattern.compile("%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])");

  private static final ImmutableMap<TypeKind, String> BOXED_TYPE_NAMES;

  static {
    EnumMap<TypeKind, String> boxedTypeNames = new EnumMap<>(TypeKind.class);
    boxedTypeNames.put(TypeKind.BYTE, Byte.class.getName());
    boxedTypeNames.put(TypeKind.SHORT, Short.class.getName());
    boxedTypeNames.put(TypeKind.INT, Integer.class.getName());
    boxedTypeNames.put(TypeKind.LONG, Long.class.getName());
    boxedTypeNames.put(TypeKind.FLOAT, Float.class.getName());
    boxedTypeNames.put(TypeKind.DOUBLE, Double.class.getName());
    boxedTypeNames.put(TypeKind.BOOLEAN, Boolean.class.getName());
    boxedTypeNames.put(TypeKind.CHAR, Character.class.getName());
    boxedTypeNames.put(TypeKind.NULL, Object.class.getName());
    BOXED_TYPE_NAMES = Maps.immutableEnumMap(boxedTypeNames);
  }

  // get type name in format accepted by Formatter.check
  private static String getFormatterType(Type type) {
    String boxedTypeName = BOXED_TYPE_NAMES.get(type.getKind());
    String typeName = (boxedTypeName != null ? boxedTypeName : type.toString());
    return ("L" + typeName.replace('.', '/') + ";");
  }

  @Override
  public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
    FormatParameters parameters = findFormatParameters(tree, state);
    if (parameters == null) {
      return Description.NO_MATCH;
    }

    if (!isVariadicInvocation(tree, state, parameters)) {
      return Description.NO_MATCH;
    }

    List<? extends ExpressionTree> args = tree.getArguments();
    if (args.get(parameters.getFormatIndex()).getKind() != Kind.STRING_LITERAL) {
      return Description.NO_MATCH;
    }

    try {
      return checkFormatString(tree, state, parameters);
    } catch (UnsupportedOperationException e) {
      return Description.NO_MATCH;
    }
  }

  private Description checkFormatString(
      MethodInvocationTree tree, VisitorState state, FormatParameters parameters) {

    List<String> errors = new ArrayList<>();
    int formatIndex = parameters.getFormatIndex();
    List<? extends ExpressionTree> args = tree.getArguments();
    JCLiteral format = (JCLiteral) args.get(formatIndex);
    String formatString = (String) format.getValue();

    List<ExpressionTree> leadingArguments = new ArrayList<>();
    List<ExpressionTree> formatArguments = new ArrayList<>();

    for (int i = 0; i < args.size(); i++) {
      if (i < formatIndex) {
        leadingArguments.add(args.get(i));
      } else if (i > formatIndex) {
        formatArguments.add(args.get(i));
      }
    }

    // Automatically switch method if this is the wrong type of format string
    boolean rewriteMethod = changeFormatTypeIfRequired(parameters, formatString);
    if (rewriteMethod) {
        errors.add("uses the wrong method for the format type");
    }

    Exception formatException = null;
    try {
      if (parameters.getType() == FormatType.MESSAGEFORMAT) {
        new MessageFormat(formatString);
      } else if (parameters.getType() == FormatType.PRINTF) {
        verifyPrintf(tree, parameters);
      }
    } catch (Exception e) {
      formatException = e;
    }
    if (formatException != null) {
      String customMessage = "Format string is invalid";
      if (formatException.getMessage() != null) {
        customMessage += ": " + formatException.getMessage();
      }
      return Description.builder(tree, pattern)
          .setMessage(customMessage)
          .build();
    }

    // Are there format string references that aren't provided?
    Set<Integer> referencedArguments = getReferencedArguments(parameters.getType(), formatString);
    if (referencesUnspecifiedArguments(referencedArguments, formatArguments.size())) {
      return describeMatch(tree);
    }

    // Are there parameters that aren't referenced in a MessageFormat?
    if (parameters.getType() == FormatType.MESSAGEFORMAT
        && referencedArguments.size() < formatArguments.size()) {

      // Could it be because the user is unaware of magic single quotes?
      String quotedString = formatString.replace("'", "''");

      if (hasQuotedArguments(formatString)) {
        Set<Integer> updatedReferences =
            getReferencedArguments(parameters.getType(), quotedString);

        if (updatedReferences.size() > referencedArguments.size()) {
          formatString = quotedString;
          errors.add("has arguments masked by single quotes");
          referencedArguments = updatedReferences;
        }
      }

      if (hasUnmatchedQuotes(formatString)) {
        formatString = quotedString;
        errors.add("has unmatched single quotes");
      }
    }

    // Could we reorder a final, unreferenced exception?
    if (parameters.getAllowExceptionReordering()
        && referencedArguments.size() < formatArguments.size()
        && max(referencedArguments) < formatArguments.size() - 1) {

      ExpressionTree last = formatArguments.get(formatArguments.size() - 1);

      if (isThrowable.matches(last, state)) {
        leadingArguments.add(last);
        formatArguments.remove(formatArguments.size() - 1);
        errors.add("ignores the passed exception");
      } else if (last instanceof MethodInvocationTree
          && isThrowableMessage.matches((MethodInvocationTree) last, state)) {
        ExpressionTree target = getInvocationTarget((MethodInvocationTree) last);
        if (target != null) {
          leadingArguments.add(target);
          formatArguments.remove(formatArguments.size() - 1);
          errors.add("ignores the passed exception message");
        }
      }
    }

    // After quoting and reordering, are there any more unreferenced parameters?
    if (referencedArguments.size() < formatArguments.size()) {
      // Then let's add them to the format string
      List<String> additionalArgs = new ArrayList<>();
      for (int i = 0; i < formatArguments.size(); i++) {
        String arg = parameters.getType() == FormatType.MESSAGEFORMAT ?
            "{" + i + "}" : "%s";
        if (!referencedArguments.contains(i)) {
          additionalArgs.add(arg);
        }
      }
      formatString = formatString + " (" + join(", ", additionalArgs) + ")";
      errors.add("ignores some parameters");
    }

    if (!errors.isEmpty()) {
      List<String> newParameters = new ArrayList<>();
      for (ExpressionTree t : leadingArguments) {
        newParameters.add(t.toString());
      }
      newParameters.add(makeLiteral(state, formatString));
      for (ExpressionTree t : formatArguments) {
        newParameters.add(t.toString());
      }

      // logger.method(param1, param2)
      //       ^-- methodStart      ^-- parameterEnd
      int methodStart = state.getEndPosition((JCTree) getInvocationTarget(tree));
      int parameterEnd = state.getEndPosition((JCTree) args.get(args.size() - 1));

      Description.Builder descriptionBuilder = Description.builder(tree, pattern)
          .setMessage("This call " + join(", ", errors));
      if (methodStart >= 0 && parameterEnd >= 0) {
        String replacement = "." + parameters.getMethodName() + "(" +  join(", ", newParameters);
        descriptionBuilder.addFix(SuggestedFix.replace(methodStart, parameterEnd, replacement));
      }
      return descriptionBuilder.build();
    }

    return Description.NO_MATCH;
  }

  // Check whether this is call is made variadically or through a final Object array parameter
  private boolean isVariadicInvocation(
      MethodInvocationTree tree, VisitorState state, FormatParameters params) {
    List<? extends ExpressionTree> arguments = tree.getArguments();
    if (arguments.size() == params.getFormatIndex() + 2) {
      ExpressionTree lastArgument = arguments.get(arguments.size() - 1);
      return !isArrayType().matches(lastArgument, state)
          || isPrimitiveArrayType().matches(lastArgument, state);
    }
    return true;
  }

  private ExpressionTree getInvocationTarget(MethodInvocationTree invocation) {
    if (invocation.getMethodSelect() instanceof MemberSelectTree) {
      return ((MemberSelectTree) invocation.getMethodSelect()).getExpression();
    }
    return null;
  }

  // Run the FindBugs checker on the string, to catch anything we don't currently detect.
  private void verifyPrintf(MethodInvocationTree tree, FormatParameters parameters)
      throws FormatFlagsConversionMismatchException, IllegalFormatException, FormatterException {
     List<? extends ExpressionTree> args = tree.getArguments();

    JCLiteral format = (JCLiteral) args.get(parameters.getFormatIndex());
    String formatString = (String) format.getValue();

    List<String> argTypes = new ArrayList<>();
    for (int i = parameters.getFormatIndex() + 1; i < args.size(); ++i) {
      Type type = ((JCExpression) args.get(i)).type;
      argTypes.add(getFormatterType(type));
    }

    try {
      Formatter.check(formatString, argTypes.toArray(new String[0]));
    } catch (ExtraFormatArgumentsException e) {
      return; // We can handle this.
    }
  }

  private boolean referencesUnspecifiedArguments(Set<Integer> usedReferences, int argumentCount) {
    for (int i : usedReferences) {
      if (i > argumentCount) {
        return true;
      }
    }
    return false;
  }

  // This is Joiner.on(separator).join(strings) but avoids pulling in Guava Collections
  private static String join(String separator, Iterable<String> strings) {
    StringBuilder builder = new StringBuilder();
    for (String s : strings) {
      builder.append(s).append(separator);
    }
    return builder.length() == 0 ? "" :
        builder.substring(0, builder.length() - separator.length());
  }

  private Set<Integer> getReferencedArguments(FormatType type, String string) {
    if (type == FormatType.PRINTF) {
      return getReferencedArgumentsP(string);
    } else if (type == FormatType.MESSAGEFORMAT) {
      return getReferencedArgumentsM(string);
    } else {
      throw new IllegalArgumentException();
    }
  }

  private Set<Integer> getReferencedArgumentsP(String str) {
    java.util.regex.Matcher matcher = printfGroup.matcher(str);

    Set<Integer> set = new HashSet<>();
    int i = 0;
    while (matcher.find()) {
      // %n is line break and %% is literal percent, they don't reference parameters.
      if (!matcher.group().endsWith("n") && !matcher.group().endsWith("%")) {
        if (matcher.group(1) != null) {
          set.add(Integer.parseInt(matcher.group(1).replaceAll("\\$", "")));
        } else {
          set.add(i);
          i++;
        }
      }
    }
    return set;
  }

  private Set<Integer> getReferencedArgumentsM(String str) {
    str = str.replaceAll("''", "")// remove literal single quotes
    str = literalRegion.matcher(str).replaceAll("");
    java.util.regex.Matcher matcher = messageFormatGroup.matcher(str);

    Set<Integer> references = new HashSet<>();
    while (matcher.find()) {
      references.add(Integer.parseInt(matcher.group(1)));
    }
    return references;
  }

  private static int max(Collection<Integer> ints) {
    return ints.isEmpty() ? -1 : Collections.max(ints);
  }

  private boolean hasQuotedArguments(String string) {
    string = string.replaceAll("''", "")// remove literal single quotes
    java.util.regex.Matcher match = literalRegion.matcher(string);
    while (match.find()) {
      if (messageFormatGroup.matcher(match.group()).find()) {
        return true;
      }
    }
    return false;
  }

  private static boolean hasUnmatchedQuotes(String string) {
    return CharMatcher.is('\'').countIn(string) % 2 == 1;
  }

  /** Given a method invocation, find whether this is a supported formatting call,
   * if so which kind and which argument is the format string. */
  private FormatParameters findFormatParameters(
      MethodInvocationTree tree, VisitorState state) {

    FormatParameters parameters = new FormatParameters();

    if (isFormattingLogger.matches(tree, state)) {
      String methodName = ASTHelpers.getSymbol(tree).toString();
      java.util.regex.Matcher matcher = formattingLoggerMethods.matcher(methodName);

      if (matcher.matches()) {
        int formatIndex = 0;
        if (matcher.group(LEVELPARAM_GROUP) != null) {
          formatIndex++;
        }
        if (matcher.group(THROWABLEPARAM_GROUP) != null) {
          formatIndex++;
        } else {
          parameters.setAllowExceptionReordering(true);
        }

        parameters.setFormatIndex(formatIndex);
        if (matcher.group(FORMAT_GROUP) == null) {
          parameters.setType(FormatType.MESSAGEFORMAT);
        } else {
          parameters.setType(FormatType.PRINTF);
        }

        parameters.setMethodBase(matcher.group(BASE_GROUP));
        return parameters;
      }
    }
    return null;
  }

  private String makeLiteral(VisitorState state, String value) {
    return TreeMaker.instance(state.context).Literal(value).toString()
        .replaceAll("\\\\'", "'"); // Humans wouldn't escape ' inside double quotes.
  }

  /** Change the FormattingParameters to a different format type if the format string
   * doesn't match the method's expected input. Returns true if a change was made. */
  private boolean changeFormatTypeIfRequired(FormatParameters parameters, String formatString) {
    if (parameters.getType() == FormatType.MESSAGEFORMAT
        && !mayBeMessageFormat(formatString) && mayBePrintfFormat(formatString)) {
      parameters.setType(FormatType.PRINTF);
      return true;
    }
    if (parameters.getType() == FormatType.PRINTF
        && !mayBePrintfFormat(formatString) && mayBeMessageFormat(formatString)) {
      parameters.setType(FormatType.MESSAGEFORMAT);
      return true;
    }
    return false;
  }

  private boolean mayBePrintfFormat(String formatString) {
    return printfGroup.matcher(formatString).find();
  }

  private boolean mayBeMessageFormat(String formatString) {
    return messageFormatGroup.matcher(formatString).find();
  }

  private static class FormatParameters {
    private FormatType type;
    private int formatIndex;
    private String methodBase;

    // If the last parameter is an exception, can we move it?
    private boolean allowExceptionReordering;

    int getFormatIndex() {
      return formatIndex;
    }

    FormatParameters setFormatIndex(int formatIndex) {
      this.formatIndex = formatIndex;
      return this;
    }

    boolean getAllowExceptionReordering() {
      return allowExceptionReordering;
    }

    FormatParameters setAllowExceptionReordering(boolean allow) {
      allowExceptionReordering = allow;
      return this;
    }

    FormatType getType() {
      return type;
    }

    FormatParameters setType(FormatType type) {
      this.type = type;
      return this;
    }

    String getMethodBase() {
      return methodBase;
    }

    FormatParameters setMethodBase(String methodBase) {
      this.methodBase = methodBase;
      return this;
    }

    String getMethodName() {
      return getMethodBase() + (type == FormatType.PRINTF ? "fmt" : "");
    }
  }

  static enum FormatType {
    PRINTF,
    MESSAGEFORMAT
  }
}
TOP

Related Classes of com.google.errorprone.bugpatterns.MisusedFormattingLogger

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.