blob: e8ee3d33e5273a419a320594518fc83b9b57e1ff [file] [log] [blame]
package org.checkerframework.checker.i18nformatter.qual;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.StringJoiner;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.framework.qual.AnnotatedFor;
/**
* Elements of this enumeration are used in a {@link I18nFormat} annotation to indicate the valid
* types that may be passed as a format parameter. For example:
*
* <pre>{@literal @}I18nFormat({GENERAL, NUMBER}) String f = "{0}{1, number}";
* MessageFormat.format(f, "Example", 0) // valid</pre>
*
* The annotation indicates that the format string requires any object as the first parameter
* ({@link I18nConversionCategory#GENERAL}) and a number as the second parameter ({@link
* I18nConversionCategory#NUMBER}).
*
* @checker_framework.manual #i18n-formatter-checker Internationalization Format String Checker
*/
@AnnotatedFor("nullness")
public enum I18nConversionCategory {
/**
* Use if a parameter is not used by the formatter. For example, in
*
* <pre>
* MessageFormat.format(&quot;{1}&quot;, a, b);
* </pre>
*
* only the second argument ("b") is used. The first argument ("a") is ignored.
*/
UNUSED(null /* everything */, null),
/** Use if the parameter can be of any type. */
GENERAL(null /* everything */, null),
/** Use if the parameter can be of date, time, or number types. */
DATE(new Class<?>[] {Date.class, Number.class}, new String[] {"date", "time"}),
/**
* Use if the parameter can be of number or choice types. An example of choice:
*
* <pre>{@code
* format("{0, choice, 0#zero|1#one|1<{0, number} is more than 1}", 2)
* }</pre>
*
* This will print "2 is more than 1".
*/
NUMBER(new Class<?>[] {Number.class}, new String[] {"number", "choice"});
@SuppressWarnings("ImmutableEnumChecker") // TODO: clean this up!
public final Class<?> @Nullable [] types;
@SuppressWarnings("ImmutableEnumChecker") // TODO: clean this up!
public final String @Nullable [] strings;
I18nConversionCategory(Class<?> @Nullable [] types, String @Nullable [] strings) {
this.types = types;
this.strings = strings;
}
/** Used by {@link #stringToI18nConversionCategory}. */
static I18nConversionCategory[] namedCategories = new I18nConversionCategory[] {DATE, NUMBER};
/**
* Creates a conversion cagetogry from a string name.
*
* <pre>
* I18nConversionCategory.stringToI18nConversionCategory("number") == I18nConversionCategory.NUMBER;
* </pre>
*
* @return the I18nConversionCategory associated with the given string
*/
@SuppressWarnings(
"nullness:iterating.over.nullable") // in namedCategories, `strings` field is non-null
public static I18nConversionCategory stringToI18nConversionCategory(String string) {
string = string.toLowerCase();
for (I18nConversionCategory v : namedCategories) {
for (String s : v.strings) {
if (s.equals(string)) {
return v;
}
}
}
throw new IllegalArgumentException("Invalid format type " + string);
}
private static <E> Set<E> arrayToSet(E[] a) {
return new HashSet<>(Arrays.asList(a));
}
/**
* Return true if a is a subset of b.
*
* @return true if a is a subset of b
*/
public static boolean isSubsetOf(I18nConversionCategory a, I18nConversionCategory b) {
return intersect(a, b) == a;
}
/**
* Returns the intersection of the two given I18nConversionCategories.
*
* <blockquote>
*
* <pre>
* I18nConversionCategory.intersect(DATE, NUMBER) == NUMBER;
* </pre>
*
* </blockquote>
*/
public static I18nConversionCategory intersect(
I18nConversionCategory a, I18nConversionCategory 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 only null in UNUSED and GENERAL
)
Set<Class<?>> as = arrayToSet(a.types);
@SuppressWarnings("nullness:argument" // types field is only null in UNUSED and GENERAL
)
Set<Class<?>> bs = arrayToSet(b.types);
as.retainAll(bs); // intersection
for (I18nConversionCategory v : new I18nConversionCategory[] {DATE, NUMBER}) {
@SuppressWarnings("nullness:argument") // in those values, `types` field is non-null
Set<Class<?>> vs = arrayToSet(v.types);
if (vs.equals(as)) {
return v;
}
}
throw new RuntimeException();
}
/**
* Returns the union of the two given I18nConversionCategories.
*
* <pre>
* I18nConversionCategory.intersect(DATE, NUMBER) == DATE;
* </pre>
*/
public static I18nConversionCategory union(I18nConversionCategory a, I18nConversionCategory b) {
if (a == UNUSED || b == UNUSED) {
return UNUSED;
}
if (a == GENERAL || b == GENERAL) {
return GENERAL;
}
if (a == DATE || b == DATE) {
return DATE;
}
return NUMBER;
}
/**
* 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 I18nConversionCategory}. */
@Override
public String toString() {
StringBuilder sb = new StringBuilder(this.name());
if (this.types == null) {
sb.append(" conversion category (all types)");
} else {
StringJoiner sj = new StringJoiner(", ", " conversion category (one of: ", ")");
for (Class<?> cls : this.types) {
sj.add(cls.getCanonicalName());
}
sb.append(sj);
}
return sb.toString();
}
}