Add ordering semantics for cached options data
This lets us easily get all options classes of a parser, or all fields of those options classes, in a deterministic way.
RELNOTES: None
PiperOrigin-RevId: 153376699
GitOrigin-RevId: 91e4dedd402c214a815eb6e2290998dec1f3d397
Change-Id: I9570bd2389ff177f014b86928bcb3194f3175049
diff --git a/java/com/google/devtools/common/options/IsolatedOptionsData.java b/java/com/google/devtools/common/options/IsolatedOptionsData.java
index 084daad..0dc787c 100644
--- a/java/com/google/devtools/common/options/IsolatedOptionsData.java
+++ b/java/com/google/devtools/common/options/IsolatedOptionsData.java
@@ -16,73 +16,89 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
+import com.google.common.collect.Ordering;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.concurrent.Immutable;
/**
- * An immutable selection of options data corresponding to a set of options classes. The data is
- * collected using reflection, which can be expensive. Therefore this class can be used internally
- * to cache the results.
+ * A selection of options data corresponding to a set of {@link OptionsBase} subclasses (options
+ * classes). The data is collected using reflection, which can be expensive. Therefore this class
+ * can be used internally to cache the results.
*
- * <p>The data is isolated in the sense that it has not yet been processed to add inter-option-
- * dependent information -- namely, the results of evaluating expansion functions. The {@link
- * OptionsData} subclass stores this added information. The reason for the split is so that we can
- * avoid exposing to expansion functions the effects of evaluating other expansion functions, to
- * ensure that the order in which they run is not significant.
+ * <p>The data is isolated in the sense that it has not yet been processed to add
+ * inter-option-dependent information -- namely, the results of evaluating expansion functions. The
+ * {@link OptionsData} subclass stores this added information. The reason for the split is so that
+ * we can avoid exposing to expansion functions the effects of evaluating other expansion functions,
+ * to ensure that the order in which they run is not significant.
+ *
+ * <p>This class is immutable so long as the converters and default values associated with the
+ * options are immutable.
*/
-// TODO(brandjon): This class is technically not necessarily immutable due to optionsDefault
-// accepting Object values, and the List in allOptionsField should be ImmutableList. Either fix
-// this or remove @Immutable.
@Immutable
public class IsolatedOptionsData extends OpaqueOptionsData {
/**
- * These are the options-declaring classes which are annotated with {@link Option} annotations.
+ * Mapping from each options class to its no-arg constructor. Entries appear in the same order
+ * that they were passed to {@link #from(Collection)}.
*/
private final ImmutableMap<Class<? extends OptionsBase>, Constructor<?>> optionsClasses;
- /** Maps option name to Option-annotated Field. */
+ /**
+ * Mapping from option name to {@code @Option}-annotated field. Entries appear ordered first by
+ * their options class (the order in which they were passed to {@link #from(Collection)}, and then
+ * in alphabetic order within each options class.
+ */
private final ImmutableMap<String, Field> nameToField;
- /** Maps option abbreviation to Option-annotated Field. */
+ /** Mapping from option abbreviation to {@code Option}-annotated field (unordered). */
private final ImmutableMap<Character, Field> abbrevToField;
- /** For each options class, contains a list of all Option-annotated fields in that class. */
- private final ImmutableMap<Class<? extends OptionsBase>, List<Field>> allOptionsFields;
+ /**
+ * Mapping from options class to a list of all {@code Option}-annotated fields in that class. The
+ * map entries are unordered, but the fields in the lists are ordered alphabetically.
+ */
+ private final ImmutableMap<Class<? extends OptionsBase>, ImmutableList<Field>> allOptionsFields;
- /** Mapping from each Option-annotated field to the default value for that field. */
- // Immutable like the others, but uses Collections.unmodifiableMap because of null values.
+ /**
+ * Mapping from each {@code Option}-annotated field to the default value for that field
+ * (unordered).
+ *
+ * <p>(This is immutable like the others, but uses {@code Collections.unmodifiableMap} to support
+ * null values.)
+ */
private final Map<Field, Object> optionDefaults;
/**
- * Mapping from each Option-annotated field to the proper converter.
+ * Mapping from each {@code Option}-annotated field to the proper converter (unordered).
*
* @see #findConverter
*/
private final ImmutableMap<Field, Converter<?>> converters;
/**
- * Mapping from each Option-annotated field to a boolean for whether that field allows multiple
- * values.
+ * Mapping from each {@code Option}-annotated field to a boolean for whether that field allows
+ * multiple values (unordered).
*/
private final ImmutableMap<Field, Boolean> allowMultiple;
private IsolatedOptionsData(
- Map<Class<? extends OptionsBase>, Constructor<?>> optionsClasses,
+ Map<Class<? extends OptionsBase>,
+ Constructor<?>> optionsClasses,
Map<String, Field> nameToField,
Map<Character, Field> abbrevToField,
- Map<Class<? extends OptionsBase>, List<Field>> allOptionsFields,
+ Map<Class<? extends OptionsBase>, ImmutableList<Field>> allOptionsFields,
Map<Field, Object> optionDefaults,
Map<Field, Converter<?>> converters,
Map<Field, Boolean> allowMultiple) {
@@ -107,6 +123,10 @@
other.allowMultiple);
}
+ /**
+ * Returns all options classes indexed by this options data object, in the order they were passed
+ * to {@link #from(Collection)}.
+ */
public Collection<Class<? extends OptionsBase>> getOptionsClasses() {
return optionsClasses.keySet();
}
@@ -120,6 +140,11 @@
return nameToField.get(name);
}
+ /**
+ * Returns all pairs of option names (not field names) and their corresponding {@link Field}
+ * objects. Entries appear ordered first by their options class (the order in which they were
+ * passed to {@link #from(Collection)}, and then in alphabetic order within each options class.
+ */
public Iterable<Map.Entry<String, Field>> getAllNamedFields() {
return nameToField.entrySet();
}
@@ -128,7 +153,11 @@
return abbrevToField.get(abbrev);
}
- public List<Field> getFieldsForClass(Class<? extends OptionsBase> optionsClass) {
+ /**
+ * Returns a list of all {@link Field} objects for options in the given options class, ordered
+ * alphabetically by option name.
+ */
+ public ImmutableList<Field> getFieldsForClass(Class<? extends OptionsBase> optionsClass) {
return allOptionsFields.get(optionsClass);
}
@@ -226,17 +255,29 @@
}
}
- private static List<Field> getAllAnnotatedFields(Class<? extends OptionsBase> optionsClass) {
- List<Field> allFields = Lists.newArrayList();
+ private static final Ordering<Field> fieldOrdering =
+ new Ordering<Field>() {
+ @Override
+ public int compare(Field f1, Field f2) {
+ String n1 = f1.getAnnotation(Option.class).name();
+ String n2 = f2.getAnnotation(Option.class).name();
+ return n1.compareTo(n2);
+ }
+ };
+
+ /**
+ * Return all {@code @Option}-annotated fields, alphabetically ordered by their option name (not
+ * their field name).
+ */
+ private static ImmutableList<Field> getAllAnnotatedFieldsSorted(
+ Class<? extends OptionsBase> optionsClass) {
+ List<Field> unsortedFields = new ArrayList<>();
for (Field field : optionsClass.getFields()) {
if (field.isAnnotationPresent(Option.class)) {
- allFields.add(field);
+ unsortedFields.add(field);
}
}
- if (allFields.isEmpty()) {
- throw new IllegalStateException(optionsClass + " has no public @Option-annotated fields");
- }
- return ImmutableList.copyOf(allFields);
+ return fieldOrdering.immutableSortedCopy(unsortedFields);
}
private static Object retrieveDefaultFromAnnotation(Field optionField) {
@@ -300,23 +341,25 @@
// Record that the boolean option takes up additional namespace for its negating alias.
booleanAliasMap.put("no" + optionName, optionName);
}
-
+
/**
* Constructs an {@link IsolatedOptionsData} object for a parser that knows about the given
* {@link OptionsBase} classes. No inter-option analysis is done. Performs basic sanity checking
* on each option in isolation.
*/
static IsolatedOptionsData from(Collection<Class<? extends OptionsBase>> classes) {
- Map<Class<? extends OptionsBase>, Constructor<?>> constructorBuilder = Maps.newHashMap();
- Map<Class<? extends OptionsBase>, List<Field>> allOptionsFieldsBuilder = Maps.newHashMap();
- Map<String, Field> nameToFieldBuilder = Maps.newHashMap();
- Map<Character, Field> abbrevToFieldBuilder = Maps.newHashMap();
- Map<Field, Object> optionDefaultsBuilder = Maps.newHashMap();
- Map<Field, Converter<?>> convertersBuilder = Maps.newHashMap();
- Map<Field, Boolean> allowMultipleBuilder = Maps.newHashMap();
+ // Mind which fields have to preserve order.
+ Map<Class<? extends OptionsBase>, Constructor<?>> constructorBuilder = new LinkedHashMap<>();
+ Map<Class<? extends OptionsBase>, ImmutableList<Field>> allOptionsFieldsBuilder =
+ new HashMap<>();
+ Map<String, Field> nameToFieldBuilder = new LinkedHashMap<>();
+ Map<Character, Field> abbrevToFieldBuilder = new HashMap<>();
+ Map<Field, Object> optionDefaultsBuilder = new HashMap<>();
+ Map<Field, Converter<?>> convertersBuilder = new HashMap<>();
+ Map<Field, Boolean> allowMultipleBuilder = new HashMap<>();
// Maps the negated boolean flag aliases to the original option name.
- Map<String, String> booleanAliasMap = Maps.newHashMap();
+ Map<String, String> booleanAliasMap = new HashMap<>();
// Read all Option annotations:
for (Class<? extends OptionsBase> parsedOptionsClass : classes) {
@@ -328,7 +371,7 @@
throw new IllegalArgumentException(parsedOptionsClass
+ " lacks an accessible default constructor");
}
- List<Field> fields = getAllAnnotatedFields(parsedOptionsClass);
+ ImmutableList<Field> fields = getAllAnnotatedFieldsSorted(parsedOptionsClass);
allOptionsFieldsBuilder.put(parsedOptionsClass, fields);
for (Field field : fields) {
diff --git a/java/com/google/devtools/common/options/OptionsUsage.java b/java/com/google/devtools/common/options/OptionsUsage.java
index aa48cb7..b898ae6 100644
--- a/java/com/google/devtools/common/options/OptionsUsage.java
+++ b/java/com/google/devtools/common/options/OptionsUsage.java
@@ -270,6 +270,7 @@
}
}
+ // TODO(brandjon): Should this use sorting by option name instead of field name?
private static final Comparator<Field> BY_NAME = new Comparator<Field>() {
@Override
public int compare(Field left, Field right) {