diff options
Diffstat (limited to 'java/com/google/devtools/common/options/OptionsData.java')
-rw-r--r-- | java/com/google/devtools/common/options/OptionsData.java | 307 |
1 files changed, 58 insertions, 249 deletions
diff --git a/java/com/google/devtools/common/options/OptionsData.java b/java/com/google/devtools/common/options/OptionsData.java index ac23d63..e71321c 100644 --- a/java/com/google/devtools/common/options/OptionsData.java +++ b/java/com/google/devtools/common/options/OptionsData.java @@ -1,4 +1,4 @@ -// Copyright 2014 The Bazel Authors. All rights reserved. +// Copyright 2017 The Bazel Authors. 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. @@ -14,280 +14,89 @@ package com.google.devtools.common.options; -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 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.Collection; -import java.util.Collections; -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. + * This extends IsolatedOptionsData with information that can only be determined once all the {@link + * OptionsBase} subclasses for a parser are known. In particular, this includes expansion + * information. */ @Immutable -final class OptionsData extends OpaqueOptionsData { - - /** - * These are the options-declaring classes which are annotated with - * {@link Option} annotations. - */ - private final Map<Class<? extends OptionsBase>, Constructor<?>> optionsClasses; - - /** Maps option name to Option-annotated Field. */ - private final Map<String, Field> nameToField; - - /** Maps option abbreviation to Option-annotated Field. */ - private final Map<Character, Field> abbrevToField; +final class OptionsData extends IsolatedOptionsData { /** - * For each options class, contains a list of all Option-annotated fields in - * that class. + * Mapping from each Option-annotated field with a {@code String[]} expansion to that expansion. */ - private final Map<Class<? extends OptionsBase>, List<Field>> allOptionsFields; + // TODO(brandjon): This is technically not necessarily immutable due to String[], and should use + // ImmutableList. Either fix this or remove @Immutable. + private final ImmutableMap<Field, String[]> evaluatedExpansions; + + /** Construct {@link OptionsData} by extending an {@link IsolatedOptionsData} with new info. */ + private OptionsData(IsolatedOptionsData base, Map<Field, String[]> evaluatedExpansions) { + super(base); + this.evaluatedExpansions = ImmutableMap.copyOf(evaluatedExpansions); + } - /** - * Mapping from each Option-annotated field to the default value for that - * field. - */ - private final Map<Field, Object> optionDefaults; + private static final String[] EMPTY_EXPANSION = new String[] {}; /** - * Mapping from each Option-annotated field to the proper converter. - * - * @see OptionsParserImpl#findConverter + * Returns the expansion of an options field, regardless of whether it was defined using {@link + * Option#expansion} or {@link Option#expansionFunction}. If the field is not an expansion option, + * returns an empty array. */ - private final Map<Field, Converter<?>> converters; + public String[] getEvaluatedExpansion(Field field) { + String[] result = evaluatedExpansions.get(field); + return result != null ? result : EMPTY_EXPANSION; + } /** - * Mapping from each Option-annotated field to a boolean for whether that field allows multiple - * values. + * Constructs an {@link OptionsData} object for a parser that knows about the given {@link + * OptionsBase} classes. In addition to the work done to construct the {@link + * IsolatedOptionsData}, this also computes expansion information. */ - private final Map<Field, Boolean> allowMultiple; - - private OptionsData(Map<Class<? extends OptionsBase>, Constructor<?>> optionsClasses, - Map<String, Field> nameToField, - Map<Character, Field> abbrevToField, - Map<Class<? extends OptionsBase>, List<Field>> allOptionsFields, - Map<Field, Object> optionDefaults, - Map<Field, Converter<?>> converters, - Map<Field, Boolean> allowMultiple) { - this.optionsClasses = ImmutableMap.copyOf(optionsClasses); - this.allOptionsFields = ImmutableMap.copyOf(allOptionsFields); - this.nameToField = ImmutableMap.copyOf(nameToField); - this.abbrevToField = ImmutableMap.copyOf(abbrevToField); - // Can't use an ImmutableMap here because of null values. - this.optionDefaults = Collections.unmodifiableMap(optionDefaults); - this.converters = ImmutableMap.copyOf(converters); - this.allowMultiple = ImmutableMap.copyOf(allowMultiple); - } - - public Collection<Class<? extends OptionsBase>> getOptionsClasses() { - return optionsClasses.keySet(); - } - - @SuppressWarnings("unchecked") // The construction ensures that the case is always valid. - public <T extends OptionsBase> Constructor<T> getConstructor(Class<T> clazz) { - return (Constructor<T>) optionsClasses.get(clazz); - } - - public Field getFieldFromName(String name) { - return nameToField.get(name); - } - - public Iterable<Map.Entry<String, Field>> getAllNamedFields() { - return nameToField.entrySet(); - } - - public Field getFieldForAbbrev(char abbrev) { - return abbrevToField.get(abbrev); - } - - public List<Field> getFieldsForClass(Class<? extends OptionsBase> optionsClass) { - return allOptionsFields.get(optionsClass); - } - - public Object getDefaultValue(Field field) { - return optionDefaults.get(field); - } - - public Converter<?> getConverter(Field field) { - return converters.get(field); - } - - public boolean getAllowMultiple(Field field) { - return allowMultiple.get(field); - } - - private static List<Field> getAllAnnotatedFields(Class<? extends OptionsBase> optionsClass) { - List<Field> allFields = Lists.newArrayList(); - for (Field field : optionsClass.getFields()) { - if (field.isAnnotationPresent(Option.class)) { - allFields.add(field); - } - } - if (allFields.isEmpty()) { - throw new IllegalStateException(optionsClass + " has no public @Option-annotated fields"); - } - return ImmutableList.copyOf(allFields); - } - - private static Object retrieveDefaultFromAnnotation(Field optionField) { - Converter<?> converter = OptionsParserImpl.findConverter(optionField); - String defaultValueAsString = OptionsParserImpl.getDefaultOptionString(optionField); - // Special case for "null" - if (OptionsParserImpl.isSpecialNullDefault(defaultValueAsString, optionField)) { - return null; - } - boolean allowsMultiple = optionField.getAnnotation(Option.class).allowMultiple(); - // If the option allows multiple values then we intentionally return the empty list as - // the default value of this option since it is not always the case that an option - // that allows multiple values will have a converter that returns a list value. - if (allowsMultiple) { - return Collections.emptyList(); - } - // Otherwise try to convert the default value using the converter - Object convertedValue; - try { - convertedValue = converter.convert(defaultValueAsString); - } catch (OptionsParsingException e) { - throw new IllegalStateException("OptionsParsingException while " - + "retrieving default for " + optionField.getName() + ": " - + e.getMessage()); - } - return convertedValue; - } - - static OptionsData of(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(); - - // Read all Option annotations: - for (Class<? extends OptionsBase> parsedOptionsClass : classes) { - try { - Constructor<? extends OptionsBase> constructor = - parsedOptionsClass.getConstructor(new Class[0]); - constructorBuilder.put(parsedOptionsClass, constructor); - } catch (NoSuchMethodException e) { - throw new IllegalArgumentException(parsedOptionsClass - + " lacks an accessible default constructor"); - } - List<Field> fields = getAllAnnotatedFields(parsedOptionsClass); - allOptionsFieldsBuilder.put(parsedOptionsClass, fields); - - for (Field field : fields) { - Option annotation = field.getAnnotation(Option.class); - - // Check that the field type is a List, and that the converter - // type matches the element type of the list. - Type fieldType = field.getGenericType(); - if (annotation.allowMultiple()) { - if (!(fieldType instanceof ParameterizedType)) { - throw new AssertionError("Type of multiple occurrence option must be a List<...>"); - } - ParameterizedType pfieldType = (ParameterizedType) fieldType; - if (pfieldType.getRawType() != List.class) { - // Throw an assertion, because this indicates an undetected type - // error in the code. - throw new AssertionError("Type of multiple occurrence option must be a List<...>"); - } - fieldType = pfieldType.getActualTypeArguments()[0]; - } - - // Get the converter return type. - @SuppressWarnings("rawtypes") - Class<? extends Converter> converter = annotation.converter(); - if (converter == Converter.class) { - Converter<?> actualConverter = OptionsParserImpl.DEFAULT_CONVERTERS.get(fieldType); - if (actualConverter == null) { - throw new AssertionError("Cannot find converter for field of type " - + field.getType() + " named " + field.getName() - + " in class " + field.getDeclaringClass().getName()); - } - converter = actualConverter.getClass(); - } - if (Modifier.isAbstract(converter.getModifiers())) { - throw new AssertionError("The converter type (" + converter - + ") must be a concrete type"); - } - Type converterResultType; - try { - Method convertMethod = converter.getMethod("convert", String.class); - converterResultType = GenericTypeHelper.getActualReturnType(converter, convertMethod); - } catch (NoSuchMethodException e) { - throw new AssertionError("A known converter object doesn't implement the convert" - + " method"); - } - - if (annotation.allowMultiple()) { - if (GenericTypeHelper.getRawType(converterResultType) == List.class) { - Type elementType = - ((ParameterizedType) converterResultType).getActualTypeArguments()[0]; - if (!GenericTypeHelper.isAssignableFrom(fieldType, elementType)) { - throw new AssertionError("If the converter return type of a multiple occurance " + - "option is a list, then the type of list elements (" + fieldType + ") must be " + - "assignable from the converter list element type (" + elementType + ")"); - } - } else { - if (!GenericTypeHelper.isAssignableFrom(fieldType, converterResultType)) { - throw new AssertionError("Type of list elements (" + fieldType + - ") for multiple occurrence option must be assignable from the converter " + - "return type (" + converterResultType + ")"); - } - } - } else { - if (!GenericTypeHelper.isAssignableFrom(fieldType, converterResultType)) { - throw new AssertionError("Type of field (" + fieldType + - ") must be assignable from the converter " + - "return type (" + converterResultType + ")"); - } - } - - if (annotation.name() == null) { + public static OptionsData from(Collection<Class<? extends OptionsBase>> classes) { + IsolatedOptionsData isolatedData = IsolatedOptionsData.from(classes); + + // All that's left is to compute expansions. + Map<Field, String[]> evaluatedExpansionsBuilder = Maps.newHashMap(); + for (Map.Entry<String, Field> entry : isolatedData.getAllNamedFields()) { + Field field = entry.getValue(); + Option annotation = field.getAnnotation(Option.class); + // Determine either the hard-coded expansion, or the ExpansionFunction class. + String[] constExpansion = annotation.expansion(); + Class<? extends ExpansionFunction> expansionFunctionClass = annotation.expansionFunction(); + if (constExpansion.length > 0 && usesExpansionFunction(annotation)) { + throw new AssertionError( + "Cannot set both expansion and expansionFunction for option --" + annotation.name()); + } else if (constExpansion.length > 0) { + evaluatedExpansionsBuilder.put(field, constExpansion); + } else if (usesExpansionFunction(annotation)) { + if (Modifier.isAbstract(expansionFunctionClass.getModifiers())) { throw new AssertionError( - "Option cannot have a null name"); + "The expansionFunction type " + expansionFunctionClass + " must be a concrete type"); } - if (nameToFieldBuilder.put(annotation.name(), field) != null) { - throw new DuplicateOptionDeclarationException( - "Duplicate option name: --" + annotation.name()); - } - if (!annotation.oldName().isEmpty()) { - if (nameToFieldBuilder.put(annotation.oldName(), field) != null) { - throw new DuplicateOptionDeclarationException( - "Old option name duplicates option name: --" + annotation.oldName()); - } - } - if (annotation.abbrev() != '\0') { - if (abbrevToFieldBuilder.put(annotation.abbrev(), field) != null) { - throw new DuplicateOptionDeclarationException( - "Duplicate option abbrev: -" + annotation.abbrev()); - } + // Evaluate the ExpansionFunction. + ExpansionFunction instance; + try { + Constructor<?> constructor = expansionFunctionClass.getConstructor(); + instance = (ExpansionFunction) constructor.newInstance(); + } catch (Exception e) { + // This indicates an error in the ExpansionFunction, and should be discovered the first + // time it is used. + throw new AssertionError(e); } - - optionDefaultsBuilder.put(field, retrieveDefaultFromAnnotation(field)); - - convertersBuilder.put(field, OptionsParserImpl.findConverter(field)); - - allowMultipleBuilder.put(field, annotation.allowMultiple()); + String[] expansion = instance.getExpansion(isolatedData); + evaluatedExpansionsBuilder.put(field, expansion); } } - return new OptionsData(constructorBuilder, nameToFieldBuilder, abbrevToFieldBuilder, - allOptionsFieldsBuilder, optionDefaultsBuilder, convertersBuilder, allowMultipleBuilder); + + return new OptionsData(isolatedData, evaluatedExpansionsBuilder); } } |