Home | History | Annotate | Download | only in options
      1 // Copyright 2014 The Bazel Authors. All rights reserved.
      2 //
      3 // Licensed under the Apache License, Version 2.0 (the "License");
      4 // you may not use this file except in compliance with the License.
      5 // You may obtain a copy of the License at
      6 //
      7 //    http://www.apache.org/licenses/LICENSE-2.0
      8 //
      9 // Unless required by applicable law or agreed to in writing, software
     10 // distributed under the License is distributed on an "AS IS" BASIS,
     11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 // See the License for the specific language governing permissions and
     13 // limitations under the License.
     14 
     15 package com.google.devtools.common.options;
     16 
     17 import com.google.common.collect.ImmutableList;
     18 import com.google.common.collect.ImmutableMap;
     19 import com.google.common.collect.Ordering;
     20 import com.google.devtools.common.options.OptionsParser.ConstructionException;
     21 import java.lang.reflect.Constructor;
     22 import java.lang.reflect.Field;
     23 import java.lang.reflect.Method;
     24 import java.lang.reflect.Modifier;
     25 import java.lang.reflect.ParameterizedType;
     26 import java.lang.reflect.Type;
     27 import java.util.ArrayList;
     28 import java.util.Collection;
     29 import java.util.Collections;
     30 import java.util.HashMap;
     31 import java.util.LinkedHashMap;
     32 import java.util.List;
     33 import java.util.Map;
     34 import javax.annotation.concurrent.Immutable;
     35 
     36 /**
     37  * A selection of options data corresponding to a set of {@link OptionsBase} subclasses (options
     38  * classes). The data is collected using reflection, which can be expensive. Therefore this class
     39  * can be used internally to cache the results.
     40  *
     41  * <p>The data is isolated in the sense that it has not yet been processed to add
     42  * inter-option-dependent information -- namely, the results of evaluating expansion functions. The
     43  * {@link OptionsData} subclass stores this added information. The reason for the split is so that
     44  * we can avoid exposing to expansion functions the effects of evaluating other expansion functions,
     45  * to ensure that the order in which they run is not significant.
     46  *
     47  * <p>This class is immutable so long as the converters and default values associated with the
     48  * options are immutable.
     49  */
     50 @Immutable
     51 public class IsolatedOptionsData extends OpaqueOptionsData {
     52 
     53   /**
     54    * Mapping from each options class to its no-arg constructor. Entries appear in the same order
     55    * that they were passed to {@link #from(Collection)}.
     56    */
     57   private final ImmutableMap<Class<? extends OptionsBase>, Constructor<?>> optionsClasses;
     58 
     59   /**
     60    * Mapping from option name to {@code @Option}-annotated field. Entries appear ordered first by
     61    * their options class (the order in which they were passed to {@link #from(Collection)}, and then
     62    * in alphabetic order within each options class.
     63    */
     64   private final ImmutableMap<String, Field> nameToField;
     65 
     66   /** Mapping from option abbreviation to {@code Option}-annotated field (unordered). */
     67   private final ImmutableMap<Character, Field> abbrevToField;
     68 
     69   /**
     70    * Mapping from options class to a list of all {@code Option}-annotated fields in that class. The
     71    * map entries are unordered, but the fields in the lists are ordered alphabetically.
     72    */
     73   private final ImmutableMap<Class<? extends OptionsBase>, ImmutableList<Field>> allOptionsFields;
     74 
     75   /**
     76    * Mapping from each {@code Option}-annotated field to the default value for that field
     77    * (unordered).
     78    *
     79    * <p>(This is immutable like the others, but uses {@code Collections.unmodifiableMap} to support
     80    * null values.)
     81    */
     82   private final Map<Field, Object> optionDefaults;
     83 
     84   /**
     85    * Mapping from each {@code Option}-annotated field to the proper converter (unordered).
     86    *
     87    * @see #findConverter
     88    */
     89   private final ImmutableMap<Field, Converter<?>> converters;
     90 
     91   /**
     92    * Mapping from each {@code Option}-annotated field to a boolean for whether that field allows
     93    * multiple values (unordered).
     94    */
     95   private final ImmutableMap<Field, Boolean> allowMultiple;
     96 
     97   /**
     98    * Mapping from each options class to whether or not it has the {@link UsesOnlyCoreTypes}
     99    * annotation (unordered).
    100    */
    101   private final ImmutableMap<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypes;
    102 
    103   /** These categories used to indicate OptionUsageRestrictions, but no longer. */
    104   private static final ImmutableList<String> DEPRECATED_CATEGORIES = ImmutableList.of(
    105       "undocumented", "hidden", "internal");
    106 
    107   private IsolatedOptionsData(
    108       Map<Class<? extends OptionsBase>,
    109       Constructor<?>> optionsClasses,
    110       Map<String, Field> nameToField,
    111       Map<Character, Field> abbrevToField,
    112       Map<Class<? extends OptionsBase>, ImmutableList<Field>> allOptionsFields,
    113       Map<Field, Object> optionDefaults,
    114       Map<Field, Converter<?>> converters,
    115       Map<Field, Boolean> allowMultiple,
    116       Map<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypes) {
    117     this.optionsClasses = ImmutableMap.copyOf(optionsClasses);
    118     this.nameToField = ImmutableMap.copyOf(nameToField);
    119     this.abbrevToField = ImmutableMap.copyOf(abbrevToField);
    120     this.allOptionsFields = ImmutableMap.copyOf(allOptionsFields);
    121     // Can't use an ImmutableMap here because of null values.
    122     this.optionDefaults = Collections.unmodifiableMap(optionDefaults);
    123     this.converters = ImmutableMap.copyOf(converters);
    124     this.allowMultiple = ImmutableMap.copyOf(allowMultiple);
    125     this.usesOnlyCoreTypes = ImmutableMap.copyOf(usesOnlyCoreTypes);
    126   }
    127 
    128   protected IsolatedOptionsData(IsolatedOptionsData other) {
    129     this(
    130         other.optionsClasses,
    131         other.nameToField,
    132         other.abbrevToField,
    133         other.allOptionsFields,
    134         other.optionDefaults,
    135         other.converters,
    136         other.allowMultiple,
    137         other.usesOnlyCoreTypes);
    138   }
    139 
    140   /**
    141    * Returns all options classes indexed by this options data object, in the order they were passed
    142    * to {@link #from(Collection)}.
    143    */
    144   public Collection<Class<? extends OptionsBase>> getOptionsClasses() {
    145     return optionsClasses.keySet();
    146   }
    147 
    148   @SuppressWarnings("unchecked") // The construction ensures that the case is always valid.
    149   public <T extends OptionsBase> Constructor<T> getConstructor(Class<T> clazz) {
    150     return (Constructor<T>) optionsClasses.get(clazz);
    151   }
    152 
    153   public Field getFieldFromName(String name) {
    154     return nameToField.get(name);
    155   }
    156 
    157   /**
    158    * Returns all pairs of option names (not field names) and their corresponding {@link Field}
    159    * objects. Entries appear ordered first by their options class (the order in which they were
    160    * passed to {@link #from(Collection)}, and then in alphabetic order within each options class.
    161    */
    162   public Iterable<Map.Entry<String, Field>> getAllNamedFields() {
    163     return nameToField.entrySet();
    164   }
    165 
    166   public Field getFieldForAbbrev(char abbrev) {
    167     return abbrevToField.get(abbrev);
    168   }
    169 
    170   /**
    171    * Returns a list of all {@link Field} objects for options in the given options class, ordered
    172    * alphabetically by option name.
    173    */
    174   public ImmutableList<Field> getFieldsForClass(Class<? extends OptionsBase> optionsClass) {
    175     return allOptionsFields.get(optionsClass);
    176   }
    177 
    178   public Object getDefaultValue(Field field) {
    179     return optionDefaults.get(field);
    180   }
    181 
    182   public Converter<?> getConverter(Field field) {
    183     return converters.get(field);
    184   }
    185 
    186   public boolean getAllowMultiple(Field field) {
    187     return allowMultiple.get(field);
    188   }
    189 
    190   public boolean getUsesOnlyCoreTypes(Class<? extends OptionsBase> optionsClass) {
    191     return usesOnlyCoreTypes.get(optionsClass);
    192   }
    193 
    194   /**
    195    * For an option that does not use {@link Option#allowMultiple}, returns its type. For an option
    196    * that does use it, asserts that the type is a {@code List<T>} and returns its element type
    197    * {@code T}.
    198    */
    199   private static Type getFieldSingularType(Field field, Option annotation) {
    200     Type fieldType = field.getGenericType();
    201     if (annotation.allowMultiple()) {
    202       // If the type isn't a List<T>, this is an error in the option's declaration.
    203       if (!(fieldType instanceof ParameterizedType)) {
    204         throw new ConstructionException("Type of multiple occurrence option must be a List<...>");
    205       }
    206       ParameterizedType pfieldType = (ParameterizedType) fieldType;
    207       if (pfieldType.getRawType() != List.class) {
    208         throw new ConstructionException("Type of multiple occurrence option must be a List<...>");
    209       }
    210       fieldType = pfieldType.getActualTypeArguments()[0];
    211     }
    212     return fieldType;
    213   }
    214 
    215   /**
    216    * Returns whether a field should be considered as boolean.
    217    *
    218    * <p>Can be used for usage help and controlling whether the "no" prefix is allowed.
    219    */
    220   static boolean isBooleanField(Field field) {
    221     return field.getType().equals(boolean.class)
    222         || field.getType().equals(TriState.class)
    223         || findConverter(field) instanceof BoolOrEnumConverter;
    224   }
    225 
    226   /** Returns whether a field has Void type. */
    227   static boolean isVoidField(Field field) {
    228     return field.getType().equals(Void.class);
    229   }
    230 
    231   /** Returns whether the arg is an expansion option. */
    232   public static boolean isExpansionOption(Option annotation) {
    233     return (annotation.expansion().length > 0 || OptionsData.usesExpansionFunction(annotation));
    234   }
    235 
    236   /**
    237    * Returns whether the arg is an expansion option defined by an expansion function (and not a
    238    * constant expansion value).
    239    */
    240   static boolean usesExpansionFunction(Option annotation) {
    241     return annotation.expansionFunction() != ExpansionFunction.class;
    242   }
    243 
    244   /**
    245    * Given an {@code @Option}-annotated field, retrieves the {@link Converter} that will be used,
    246    * taking into account the default converters if an explicit one is not specified.
    247    */
    248   static Converter<?> findConverter(Field optionField) {
    249     Option annotation = optionField.getAnnotation(Option.class);
    250     if (annotation.converter() == Converter.class) {
    251       // No converter provided, use the default one.
    252       Type type = getFieldSingularType(optionField, annotation);
    253       Converter<?> converter = Converters.DEFAULT_CONVERTERS.get(type);
    254       if (converter == null) {
    255         throw new ConstructionException(
    256             "No converter found for "
    257                 + type
    258                 + "; possible fix: add "
    259                 + "converter=... to @Option annotation for "
    260                 + optionField.getName());
    261       }
    262       return converter;
    263     }
    264     try {
    265       // Instantiate the given Converter class.
    266       Class<?> converter = annotation.converter();
    267       Constructor<?> constructor = converter.getConstructor();
    268       return (Converter<?>) constructor.newInstance();
    269     } catch (Exception e) {
    270       // This indicates an error in the Converter, and should be discovered the first time it is
    271       // used.
    272       throw new ConstructionException(e);
    273     }
    274   }
    275 
    276   private static final Ordering<Field> fieldOrdering =
    277       new Ordering<Field>() {
    278     @Override
    279     public int compare(Field f1, Field f2) {
    280       String n1 = f1.getAnnotation(Option.class).name();
    281       String n2 = f2.getAnnotation(Option.class).name();
    282       return n1.compareTo(n2);
    283     }
    284   };
    285 
    286   /**
    287    * Return all {@code @Option}-annotated fields, alphabetically ordered by their option name (not
    288    * their field name).
    289    */
    290   private static ImmutableList<Field> getAllAnnotatedFieldsSorted(
    291       Class<? extends OptionsBase> optionsClass) {
    292     List<Field> unsortedFields = new ArrayList<>();
    293     for (Field field : optionsClass.getFields()) {
    294       if (field.isAnnotationPresent(Option.class)) {
    295         unsortedFields.add(field);
    296       }
    297     }
    298     return fieldOrdering.immutableSortedCopy(unsortedFields);
    299   }
    300 
    301   private static Object retrieveDefaultFromAnnotation(Field optionField) {
    302     Converter<?> converter = findConverter(optionField);
    303     String defaultValueAsString = OptionsParserImpl.getDefaultOptionString(optionField);
    304     // Special case for "null"
    305     if (OptionsParserImpl.isSpecialNullDefault(defaultValueAsString, optionField)) {
    306       return null;
    307     }
    308     boolean allowsMultiple = optionField.getAnnotation(Option.class).allowMultiple();
    309     // If the option allows multiple values then we intentionally return the empty list as
    310     // the default value of this option since it is not always the case that an option
    311     // that allows multiple values will have a converter that returns a list value.
    312     if (allowsMultiple) {
    313       return Collections.emptyList();
    314     }
    315     // Otherwise try to convert the default value using the converter
    316     Object convertedValue;
    317     try {
    318       convertedValue = converter.convert(defaultValueAsString);
    319     } catch (OptionsParsingException e) {
    320       throw new IllegalStateException("OptionsParsingException while "
    321           + "retrieving default for " + optionField.getName() + ": "
    322           + e.getMessage());
    323     }
    324     return convertedValue;
    325   }
    326 
    327   private static <A> void checkForCollisions(
    328       Map<A, Field> aFieldMap,
    329       A optionName,
    330       String description) {
    331     if (aFieldMap.containsKey(optionName)) {
    332       throw new DuplicateOptionDeclarationException(
    333           "Duplicate option name, due to " + description + ": --" + optionName);
    334     }
    335   }
    336 
    337   private static void checkForBooleanAliasCollisions(
    338       Map<String, String> booleanAliasMap,
    339       String optionName,
    340       String description) {
    341     if (booleanAliasMap.containsKey(optionName)) {
    342       throw new DuplicateOptionDeclarationException(
    343           "Duplicate option name, due to "
    344               + description
    345               + " --"
    346               + optionName
    347               + ", it conflicts with a negating alias for boolean flag --"
    348               + booleanAliasMap.get(optionName));
    349     }
    350   }
    351 
    352   private static void checkAndUpdateBooleanAliases(
    353       Map<String, Field> nameToFieldMap,
    354       Map<String, String> booleanAliasMap,
    355       String optionName) {
    356     // Check that the negating alias does not conflict with existing flags.
    357     checkForCollisions(nameToFieldMap, "no_" + optionName, "boolean option alias");
    358     checkForCollisions(nameToFieldMap, "no" + optionName, "boolean option alias");
    359 
    360     // Record that the boolean option takes up additional namespace for its negating alias.
    361     booleanAliasMap.put("no_" + optionName, optionName);
    362     booleanAliasMap.put("no" + optionName, optionName);
    363   }
    364 
    365   /**
    366    * Constructs an {@link IsolatedOptionsData} object for a parser that knows about the given
    367    * {@link OptionsBase} classes. No inter-option analysis is done. Performs basic sanity checking
    368    * on each option in isolation.
    369    */
    370   static IsolatedOptionsData from(Collection<Class<? extends OptionsBase>> classes) {
    371     // Mind which fields have to preserve order.
    372     Map<Class<? extends OptionsBase>, Constructor<?>> constructorBuilder = new LinkedHashMap<>();
    373     Map<Class<? extends OptionsBase>, ImmutableList<Field>> allOptionsFieldsBuilder =
    374         new HashMap<>();
    375     Map<String, Field> nameToFieldBuilder = new LinkedHashMap<>();
    376     Map<Character, Field> abbrevToFieldBuilder = new HashMap<>();
    377     Map<Field, Object> optionDefaultsBuilder = new HashMap<>();
    378     Map<Field, Converter<?>> convertersBuilder = new HashMap<>();
    379     Map<Field, Boolean> allowMultipleBuilder = new HashMap<>();
    380 
    381     // Maps the negated boolean flag aliases to the original option name.
    382     Map<String, String> booleanAliasMap = new HashMap<>();
    383 
    384     Map<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypesBuilder = new HashMap<>();
    385 
    386     // Read all Option annotations:
    387     for (Class<? extends OptionsBase> parsedOptionsClass : classes) {
    388       try {
    389         Constructor<? extends OptionsBase> constructor =
    390             parsedOptionsClass.getConstructor();
    391         constructorBuilder.put(parsedOptionsClass, constructor);
    392       } catch (NoSuchMethodException e) {
    393         throw new IllegalArgumentException(parsedOptionsClass
    394             + " lacks an accessible default constructor");
    395       }
    396       ImmutableList<Field> fields = getAllAnnotatedFieldsSorted(parsedOptionsClass);
    397       allOptionsFieldsBuilder.put(parsedOptionsClass, fields);
    398 
    399       for (Field field : fields) {
    400         Option annotation = field.getAnnotation(Option.class);
    401         String optionName = annotation.name();
    402         if (optionName == null) {
    403           throw new ConstructionException("Option cannot have a null name");
    404         }
    405 
    406         if (DEPRECATED_CATEGORIES.contains(annotation.category())) {
    407           throw new ConstructionException(
    408               "Documentation level is no longer read from the option category. Category \""
    409                   + annotation.category() + "\" in option \"" + optionName + "\" is disallowed.");
    410         }
    411 
    412         Type fieldType = getFieldSingularType(field, annotation);
    413 
    414         // Get the converter return type.
    415         @SuppressWarnings("rawtypes")
    416         Class<? extends Converter> converter = annotation.converter();
    417         if (converter == Converter.class) {
    418           Converter<?> actualConverter = Converters.DEFAULT_CONVERTERS.get(fieldType);
    419           if (actualConverter == null) {
    420             throw new ConstructionException("Cannot find converter for field of type "
    421                 + field.getType() + " named " + field.getName()
    422                 + " in class " + field.getDeclaringClass().getName());
    423           }
    424           converter = actualConverter.getClass();
    425         }
    426         if (Modifier.isAbstract(converter.getModifiers())) {
    427           throw new ConstructionException("The converter type " + converter
    428               + " must be a concrete type");
    429         }
    430         Type converterResultType;
    431         try {
    432           Method convertMethod = converter.getMethod("convert", String.class);
    433           converterResultType = GenericTypeHelper.getActualReturnType(converter, convertMethod);
    434         } catch (NoSuchMethodException e) {
    435           throw new ConstructionException(
    436               "A known converter object doesn't implement the convert method");
    437         }
    438 
    439         if (annotation.allowMultiple()) {
    440           if (GenericTypeHelper.getRawType(converterResultType) == List.class) {
    441             Type elementType =
    442                 ((ParameterizedType) converterResultType).getActualTypeArguments()[0];
    443             if (!GenericTypeHelper.isAssignableFrom(fieldType, elementType)) {
    444               throw new ConstructionException(
    445                   "If the converter return type of a multiple occurrence option is a list, then "
    446                       + "the type of list elements ("
    447                       + fieldType
    448                       + ") must be assignable from the converter list element type ("
    449                       + elementType
    450                       + ")");
    451             }
    452           } else {
    453             if (!GenericTypeHelper.isAssignableFrom(fieldType, converterResultType)) {
    454               throw new ConstructionException(
    455                   "Type of list elements ("
    456                       + fieldType
    457                       + ") for multiple occurrence option must be assignable from the converter "
    458                       + "return type ("
    459                       + converterResultType
    460                       + ")");
    461             }
    462           }
    463         } else {
    464           if (!GenericTypeHelper.isAssignableFrom(fieldType, converterResultType)) {
    465             throw new ConstructionException(
    466                 "Type of field ("
    467                     + fieldType
    468                     + ") must be assignable from the converter return type ("
    469                     + converterResultType
    470                     + ")");
    471           }
    472         }
    473 
    474         if (isBooleanField(field)) {
    475           checkAndUpdateBooleanAliases(nameToFieldBuilder, booleanAliasMap, optionName);
    476         }
    477 
    478         checkForCollisions(nameToFieldBuilder, optionName, "option");
    479         checkForBooleanAliasCollisions(booleanAliasMap, optionName, "option");
    480         nameToFieldBuilder.put(optionName, field);
    481 
    482         if (!annotation.oldName().isEmpty()) {
    483           String oldName = annotation.oldName();
    484           checkForCollisions(nameToFieldBuilder, oldName, "old option name");
    485           checkForBooleanAliasCollisions(booleanAliasMap, oldName, "old option name");
    486           nameToFieldBuilder.put(annotation.oldName(), field);
    487 
    488           // If boolean, repeat the alias dance for the old name.
    489           if (isBooleanField(field)) {
    490             checkAndUpdateBooleanAliases(nameToFieldBuilder, booleanAliasMap, oldName);
    491           }
    492         }
    493         if (annotation.abbrev() != '\0') {
    494           checkForCollisions(abbrevToFieldBuilder, annotation.abbrev(), "option abbreviation");
    495           abbrevToFieldBuilder.put(annotation.abbrev(), field);
    496         }
    497 
    498         optionDefaultsBuilder.put(field, retrieveDefaultFromAnnotation(field));
    499 
    500         convertersBuilder.put(field, findConverter(field));
    501 
    502         allowMultipleBuilder.put(field, annotation.allowMultiple());
    503 
    504         }
    505 
    506       boolean usesOnlyCoreTypes = parsedOptionsClass.isAnnotationPresent(UsesOnlyCoreTypes.class);
    507       if (usesOnlyCoreTypes) {
    508         // Validate that @UsesOnlyCoreTypes was used correctly.
    509         for (Field field : fields) {
    510           // The classes in coreTypes are all final. But even if they weren't, we only want to check
    511           // for exact matches; subclasses would not be considered core types.
    512           if (!UsesOnlyCoreTypes.CORE_TYPES.contains(field.getType())) {
    513             throw new ConstructionException(
    514                 "Options class '" + parsedOptionsClass.getName() + "' is marked as "
    515                 + "@UsesOnlyCoreTypes, but field '" + field.getName()
    516                 + "' has type '" + field.getType().getName() + "'");
    517           }
    518         }
    519       }
    520       usesOnlyCoreTypesBuilder.put(parsedOptionsClass, usesOnlyCoreTypes);
    521     }
    522 
    523     return new IsolatedOptionsData(
    524         constructorBuilder,
    525         nameToFieldBuilder,
    526         abbrevToFieldBuilder,
    527         allOptionsFieldsBuilder,
    528         optionDefaultsBuilder,
    529         convertersBuilder,
    530         allowMultipleBuilder,
    531         usesOnlyCoreTypesBuilder);
    532   }
    533 
    534 }
    535