/*
* Copyright 2016 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.formatstring;
import com.google.auto.value.AutoValue;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.errorprone.VisitorState;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Types;
import edu.umd.cs.findbugs.formatStringChecker.ExtraFormatArgumentsException;
import edu.umd.cs.findbugs.formatStringChecker.Formatter;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayDeque;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Deque;
import java.util.DuplicateFormatFlagsException;
import java.util.FormatFlagsConversionMismatchException;
import java.util.GregorianCalendar;
import java.util.IllegalFormatCodePointException;
import java.util.IllegalFormatConversionException;
import java.util.IllegalFormatFlagsException;
import java.util.IllegalFormatPrecisionException;
import java.util.IllegalFormatWidthException;
import java.util.MissingFormatArgumentException;
import java.util.MissingFormatWidthException;
import java.util.UnknownFormatConversionException;
import java.util.UnknownFormatFlagsException;
import javax.annotation.Nullable;
import javax.lang.model.type.TypeKind;
/** Utilities for validating format strings. */
public class FormatStringValidation {
/**
* Description of an incorrect format method call.
*/
@AutoValue
public abstract static class ValidationResult {
/** The exception thrown by {@code String.format} or {@code Formatter.check}. */
@Nullable
public abstract Exception exception();
/**
* A human-readable diagnostic message.
*/
public abstract String message();
public static ValidationResult create(@Nullable Exception exception, String message) {
return new AutoValue_FormatStringValidation_ValidationResult(exception, message);
}
}
@Nullable
public static ValidationResult validate(
Collection<? extends ExpressionTree> arguments, final VisitorState state) {
Deque<ExpressionTree> args = new ArrayDeque<ExpressionTree>(arguments);
String formatString = ASTHelpers.constValue(args.removeFirst(), String.class);
if (formatString == null) {
return null;
}
// If the only argument is an Object[], it's an explicit varargs call.
// Bail out, since we don't know what the actual argument types are.
if (args.size() == 1) {
Type type = ASTHelpers.getType(Iterables.getOnlyElement(args));
if (type instanceof Type.ArrayType
&& ASTHelpers.isSameType(
((Type.ArrayType) type).elemtype, state.getSymtab().objectType, state)) {
return null;
}
}
Iterable<Object> instances =
Iterables.transform(
args,
new Function<ExpressionTree, Object>() {
@Override
public Object apply(ExpressionTree input) {
try {
return getInstance(input, state);
} catch (Throwable t) {
// ignore symbol completion failures
return null;
}
}
});
return validate(formatString, instances);
}
/**
* Return an instance of the given type if it receives special handling by {@code String.format}.
* For example, an intance of {@link Integer} will be returned for an input of type {@code int}
* or {@link Integer}.
*/
private static Object getInstance(Tree tree, VisitorState state) {
Object value = ASTHelpers.constValue(tree);
if (value != null) {
return value;
}
Type type = ASTHelpers.getType(tree);
Types types = state.getTypes();
if (type.getKind() == TypeKind.NULL) {
return null;
}
// normalize boxed primitives
type = types.unboxedTypeOrType(types.erasure(type));
if (type.isPrimitive()) {
switch (type.getKind()) {
case BOOLEAN:
return false;
case BYTE:
return Byte.valueOf((byte) 1);
case SHORT:
return Short.valueOf((short) 2);
case INT:
return Integer.valueOf(3);
case LONG:
return Long.valueOf(4);
case CHAR:
return Character.valueOf('c');
case FLOAT:
return Float.valueOf(5.0f);
case DOUBLE:
return Double.valueOf(6.0d);
case VOID:
case NONE:
case NULL:
case ERROR:
return null;
case ARRAY:
return new Object[0];
default:
break;
}
}
if (types.isSubtype(type, state.getSymtab().stringType)) {
return String.valueOf("string");
}
if (types.isSubtype(type, state.getTypeFromString(BigDecimal.class.getName()))) {
return BigDecimal.valueOf(42.0d);
}
if (types.isSubtype(type, state.getTypeFromString(BigInteger.class.getName()))) {
return BigInteger.valueOf(43L);
}
if (types.isSubtype(type, state.getTypeFromString(Date.class.getName()))) {
return new Date();
}
if (types.isSubtype(type, state.getTypeFromString(Calendar.class.getName()))) {
return new GregorianCalendar();
}
if (types.isSubtype(type, state.getTypeFromString(TemporalAccessor.class.getName()))) {
return LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault());
}
return new Object();
}
private static ValidationResult validate(String formatString, Iterable<Object> arguments) {
try {
String unused = String.format(formatString, Iterables.toArray(arguments, Object.class));
} catch (DuplicateFormatFlagsException e) {
return ValidationResult.create(e, String.format("duplicate format flags: %s", e.getFlags()));
} catch (FormatFlagsConversionMismatchException e) {
return ValidationResult.create(
e,
String.format(
"format specifier '%%%s' is not compatible with the given flag(s): %s",
e.getConversion(), e.getFlags()));
} catch (IllegalFormatCodePointException e) {
return ValidationResult.create(
e, String.format("invalid Unicode code point: %x", e.getCodePoint()));
} catch (IllegalFormatConversionException e) {
return ValidationResult.create(
e,
String.format(
"illegal format conversion: '%s' cannot be formatted using '%%%s'",
e.getArgumentClass().getName(), e.getConversion()));
} catch (IllegalFormatFlagsException e) {
return ValidationResult.create(e, String.format("illegal format flags: %s", e.getFlags()));
} catch (IllegalFormatPrecisionException e) {
return ValidationResult.create(
e, String.format("illegal format precision: %d", e.getPrecision()));
} catch (IllegalFormatWidthException e) {
return ValidationResult.create(e, String.format("illegal format width: %s", e.getWidth()));
} catch (MissingFormatArgumentException e) {
return ValidationResult.create(
e, String.format("missing argument for format specifier '%s'", e.getFormatSpecifier()));
} catch (MissingFormatWidthException e) {
return ValidationResult.create(
e, String.format("missing format width: %s", e.getFormatSpecifier()));
} catch (UnknownFormatConversionException e) {
return ValidationResult.create(e, unknownFormatConversion(e.getConversion()));
} catch (UnknownFormatFlagsException e) {
// TODO(cushon): I don't think the implementation ever throws this.
return ValidationResult.create(e, String.format("unknown format flag(s): %s", e.getFlags()));
}
try {
// arguments are specified as type descriptors, and all we care about checking is the arity
String[] argDescriptors =
Collections.nCopies(Iterables.size(arguments), "Ljava/lang/Object;")
.toArray(new String[0]);
Formatter.check(formatString, argDescriptors);
} catch (ExtraFormatArgumentsException e) {
return ValidationResult.create(
e, String.format("extra format arguments: used %d, provided %d", e.used, e.provided));
} catch (Exception ignored) {
// everything else is validated by String.format above
}
return null;
}
private static String unknownFormatConversion(String conversion) {
if (conversion.equals("l")) {
return "%l is not a valid format specifier; use %d for all integral types and %f for all "
+ "floating point types";
}
return String.format("unknown format conversion: '%s'", conversion);
}
}