| package org.checkerframework.checker.formatter.qual; |
| |
| import java.math.BigDecimal; |
| import java.math.BigInteger; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Calendar; |
| import java.util.Date; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.StringJoiner; |
| import org.checkerframework.checker.nullness.qual.Nullable; |
| import org.checkerframework.dataflow.qual.Pure; |
| import org.checkerframework.framework.qual.AnnotatedFor; |
| |
| /** |
| * Elements of this enumeration are used in a {@link Format Format} annotation to indicate the valid |
| * types that may be passed as a format parameter. For example: |
| * |
| * <blockquote> |
| * |
| * <pre>{@literal @}Format({GENERAL, INT}) String f = "String '%s' has length %d"; |
| * |
| * String.format(f, "Example", 7);</pre> |
| * |
| * </blockquote> |
| * |
| * The annotation indicates that the format string requires any Object as the first parameter |
| * ({@link ConversionCategory#GENERAL}) and an integer as the second parameter ({@link |
| * ConversionCategory#INT}). |
| * |
| * @see Format |
| * @checker_framework.manual #formatter-checker Format String Checker |
| */ |
| @SuppressWarnings("unchecked") // ".class" expressions in varargs position |
| @AnnotatedFor("nullness") |
| public enum ConversionCategory { |
| /** Use if the parameter can be of any type. Applicable for conversions b, B, h, H, s, S. */ |
| GENERAL("bBhHsS", (Class<?>[]) null /* everything */), |
| |
| /** |
| * Use if the parameter is of a basic types which represent Unicode characters: char, Character, |
| * byte, Byte, short, and Short. This conversion may also be applied to the types int and Integer |
| * when Character.isValidCodePoint(int) returns true. Applicable for conversions c, C. |
| */ |
| CHAR("cC", Character.class, Byte.class, Short.class, Integer.class), |
| |
| /** |
| * Use if the parameter is an integral type: byte, Byte, short, Short, int and Integer, long, |
| * Long, and BigInteger. Applicable for conversions d, o, x, X. |
| */ |
| INT("doxX", Byte.class, Short.class, Integer.class, Long.class, BigInteger.class), |
| |
| /** |
| * Use if the parameter is a floating-point type: float, Float, double, Double, and BigDecimal. |
| * Applicable for conversions e, E, f, g, G, a, A. |
| */ |
| FLOAT("eEfgGaA", Float.class, Double.class, BigDecimal.class), |
| |
| /** |
| * Use if the parameter is a type which is capable of encoding a date or time: long, Long, |
| * Calendar, and Date. Applicable for conversions t, T. |
| */ |
| @SuppressWarnings("JdkObsolete") |
| TIME("tT", Long.class, Calendar.class, Date.class), |
| |
| /** |
| * Use if the parameter is both a char and an int. |
| * |
| * <p>In a format string, multiple conversions may be applied to the same parameter. This is |
| * seldom needed, but the following is an example of such use: |
| * |
| * <pre> |
| * format("Test %1$c %1$d", (int)42); |
| * </pre> |
| * |
| * In this example, the first parameter is interpreted as both a character and an int, therefore |
| * the parameter must be compatible with both conversion, and can therefore neither be char nor |
| * long. This intersection of conversions is called CHAR_AND_INT. |
| * |
| * <p>One other conversion intersection is interesting, namely the intersection of INT and TIME, |
| * resulting in INT_AND_TIME. |
| * |
| * <p>All other intersection either lead to an already existing type, or NULL, in which case it is |
| * illegal to pass object's of any type as parameter. |
| */ |
| CHAR_AND_INT(null, Byte.class, Short.class, Integer.class), |
| |
| /** |
| * Use if the parameter is both an int and a time. |
| * |
| * @see #CHAR_AND_INT |
| */ |
| INT_AND_TIME(null, Long.class), |
| |
| /** |
| * Use if no object of any type can be passed as parameter. In this case, the only legal value is |
| * null. This is seldomly needed, and indicates an error in most cases. For example: |
| * |
| * <pre> |
| * format("Test %1$f %1$d", null); |
| * </pre> |
| * |
| * Only null can be legally passed, passing a value such as 4 or 4.2 would lead to an exception. |
| */ |
| NULL(null), |
| |
| /** |
| * Use if a parameter is not used by the formatter. This is seldomly needed, and indicates an |
| * error in most cases. For example: |
| * |
| * <pre> |
| * format("Test %1$s %3$s", "a","unused","b"); |
| * </pre> |
| * |
| * Only the first "a" and third "b" parameters are used, the second "unused" parameter is ignored. |
| */ |
| UNUSED(null, (Class<?>[]) null /* everything */); |
| |
| /** The argument types. Null means every type. */ |
| @SuppressWarnings("ImmutableEnumChecker") // TODO: clean this up! |
| public final Class<?> @Nullable [] types; |
| |
| /** The format specifier characters. Null means users cannot specify it directly. */ |
| public final @Nullable String chars; |
| |
| /** |
| * Create a new conversion category. |
| * |
| * @param chars the format specifier characters. Null means users cannot specify it directly. |
| * @param types the argument types. Null means every type. |
| */ |
| ConversionCategory(@Nullable String chars, Class<?> @Nullable ... types) { |
| this.chars = chars; |
| if (types == null) { |
| this.types = types; |
| } else { |
| List<Class<?>> typesWithPrimitives = new ArrayList<>(types.length); |
| for (Class<?> type : types) { |
| typesWithPrimitives.add(type); |
| Class<?> unwrapped = unwrapPrimitive(type); |
| if (unwrapped != null) { |
| typesWithPrimitives.add(unwrapped); |
| } |
| } |
| this.types = typesWithPrimitives.toArray(new Class<?>[typesWithPrimitives.size()]); |
| } |
| } |
| |
| /** |
| * If the given class is a primitive wrapper, return the corresponding primitive class. Otherwise |
| * return null. |
| * |
| * @param c a class |
| * @return the unwrapped primitive, or null |
| */ |
| private static @Nullable Class<? extends Object> unwrapPrimitive(Class<?> c) { |
| if (c == Byte.class) { |
| return byte.class; |
| } |
| if (c == Character.class) { |
| return char.class; |
| } |
| if (c == Short.class) { |
| return short.class; |
| } |
| if (c == Integer.class) { |
| return int.class; |
| } |
| if (c == Long.class) { |
| return long.class; |
| } |
| if (c == Float.class) { |
| return float.class; |
| } |
| if (c == Double.class) { |
| return double.class; |
| } |
| if (c == Boolean.class) { |
| return boolean.class; |
| } |
| return null; |
| } |
| |
| /** |
| * Converts a conversion character to a category. For example: |
| * |
| * <pre>{@code |
| * ConversionCategory.fromConversionChar('d') == ConversionCategory.INT |
| * }</pre> |
| * |
| * @param c a conversion character |
| * @return the category for the given conversion character |
| */ |
| @SuppressWarnings("nullness:dereference.of.nullable") // `chars` field is non-null for these |
| public static ConversionCategory fromConversionChar(char c) { |
| for (ConversionCategory v : new ConversionCategory[] {GENERAL, CHAR, INT, FLOAT, TIME}) { |
| if (v.chars.contains(String.valueOf(c))) { |
| return v; |
| } |
| } |
| throw new IllegalArgumentException("Bad conversion character " + c); |
| } |
| |
| private static <E> Set<E> arrayToSet(E[] a) { |
| return new HashSet<>(Arrays.asList(a)); |
| } |
| |
| public static boolean isSubsetOf(ConversionCategory a, ConversionCategory b) { |
| return intersect(a, b) == a; |
| } |
| |
| /** |
| * Returns the intersection of two categories. This is seldomly needed. |
| * |
| * <blockquote> |
| * |
| * <pre> |
| * ConversionCategory.intersect(INT, TIME) == INT_AND_TIME; |
| * </pre> |
| * |
| * </blockquote> |
| * |
| * @param a a category |
| * @param b a category |
| * @return the intersection of the two categories (their greatest lower bound) |
| */ |
| public static ConversionCategory intersect(ConversionCategory a, ConversionCategory b) { |
| if (a == UNUSED) { |
| return b; |
| } |
| if (b == UNUSED) { |
| return a; |
| } |
| if (a == GENERAL) { |
| return b; |
| } |
| if (b == GENERAL) { |
| return a; |
| } |
| |
| @SuppressWarnings("nullness:argument" // `types` field is null only for UNUSED and GENERAL |
| ) |
| Set<Class<?>> as = arrayToSet(a.types); |
| @SuppressWarnings("nullness:argument" // `types` field is null only for UNUSED and GENERAL |
| ) |
| Set<Class<?>> bs = arrayToSet(b.types); |
| as.retainAll(bs); // intersection |
| for (ConversionCategory v : |
| new ConversionCategory[] {CHAR, INT, FLOAT, TIME, CHAR_AND_INT, INT_AND_TIME, NULL}) { |
| @SuppressWarnings("nullness:argument" // `types` field is null only for UNUSED and GENERAL |
| ) |
| Set<Class<?>> vs = arrayToSet(v.types); |
| if (vs.equals(as)) { |
| return v; |
| } |
| } |
| throw new RuntimeException(); |
| } |
| |
| /** |
| * Returns the union of two categories. This is seldomly needed. |
| * |
| * <blockquote> |
| * |
| * <pre> |
| * ConversionCategory.union(INT, TIME) == GENERAL; |
| * </pre> |
| * |
| * </blockquote> |
| * |
| * @param a a category |
| * @param b a category |
| * @return the union of the two categories (their least upper bound) |
| */ |
| public static ConversionCategory union(ConversionCategory a, ConversionCategory b) { |
| if (a == UNUSED || b == UNUSED) { |
| return UNUSED; |
| } |
| if (a == GENERAL || b == GENERAL) { |
| return GENERAL; |
| } |
| if ((a == CHAR_AND_INT && b == INT_AND_TIME) || (a == INT_AND_TIME && b == CHAR_AND_INT)) { |
| // This is special-cased because the union of a.types and b.types |
| // does not include BigInteger.class, whereas the types for INT does. |
| // Returning INT here to prevent returning GENERAL below. |
| return INT; |
| } |
| |
| @SuppressWarnings("nullness:argument" // `types` field is null only for UNUSED and GENERAL |
| ) |
| Set<Class<?>> as = arrayToSet(a.types); |
| @SuppressWarnings("nullness:argument" // `types` field is null only for UNUSED and GENERAL |
| ) |
| Set<Class<?>> bs = arrayToSet(b.types); |
| as.addAll(bs); // union |
| for (ConversionCategory v : |
| new ConversionCategory[] {NULL, CHAR_AND_INT, INT_AND_TIME, CHAR, INT, FLOAT, TIME}) { |
| @SuppressWarnings("nullness:argument" // `types` field is null only for UNUSED and GENERAL |
| ) |
| Set<Class<?>> vs = arrayToSet(v.types); |
| if (vs.equals(as)) { |
| return v; |
| } |
| } |
| |
| return GENERAL; |
| } |
| |
| /** |
| * Returns true if {@code argType} can be an argument used by this format specifier. |
| * |
| * @param argType an argument type |
| * @return true if {@code argType} can be an argument used by this format specifier |
| */ |
| public boolean isAssignableFrom(Class<?> argType) { |
| if (types == null) { |
| return true; |
| } |
| if (argType == void.class) { |
| return true; |
| } |
| for (Class<?> c : types) { |
| if (c.isAssignableFrom(argType)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** Returns a pretty printed {@link ConversionCategory}. */ |
| @Pure |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder(); |
| sb.append(name()); |
| sb.append(" conversion category"); |
| |
| if (types == null || types.length == 0) { |
| return sb.toString(); |
| } |
| |
| StringJoiner sj = new StringJoiner(", ", "(one of: ", ")"); |
| for (Class<?> cls : types) { |
| sj.add(cls.getSimpleName()); |
| } |
| sb.append(" "); |
| sb.append(sj); |
| |
| return sb.toString(); |
| } |
| } |