com.google.errorprone.bugpatterns.formatstring.FormatStringValidation.java Source code

Java tutorial

Introduction

Here is the source code for com.google.errorprone.bugpatterns.formatstring.FormatStringValidation.java

Source

/*
 * 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);
    }
}