• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
IsolatedOptionsData( Map<Class<? extends OptionsBase>, Constructor<?>> optionsClasses, Map<String, Field> nameToField, Map<Character, Field> abbrevToField, Map<Class<? extends OptionsBase>, ImmutableList<Field>> allOptionsFields, Map<Field, Object> optionDefaults, Map<Field, Converter<?>> converters, Map<Field, Boolean> allowMultiple, Map<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypes)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 
IsolatedOptionsData(IsolatedOptionsData other)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    */
getOptionsClasses()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.
getConstructor(Class<T> clazz)149   public <T extends OptionsBase> Constructor<T> getConstructor(Class<T> clazz) {
150     return (Constructor<T>) optionsClasses.get(clazz);
151   }
152 
getFieldFromName(String name)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    */
getAllNamedFields()162   public Iterable<Map.Entry<String, Field>> getAllNamedFields() {
163     return nameToField.entrySet();
164   }
165 
getFieldForAbbrev(char abbrev)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    */
getFieldsForClass(Class<? extends OptionsBase> optionsClass)174   public ImmutableList<Field> getFieldsForClass(Class<? extends OptionsBase> optionsClass) {
175     return allOptionsFields.get(optionsClass);
176   }
177 
getDefaultValue(Field field)178   public Object getDefaultValue(Field field) {
179     return optionDefaults.get(field);
180   }
181 
getConverter(Field field)182   public Converter<?> getConverter(Field field) {
183     return converters.get(field);
184   }
185 
getAllowMultiple(Field field)186   public boolean getAllowMultiple(Field field) {
187     return allowMultiple.get(field);
188   }
189 
getUsesOnlyCoreTypes(Class<? extends OptionsBase> optionsClass)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    */
getFieldSingularType(Field field, Option annotation)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    */
isBooleanField(Field field)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. */
isVoidField(Field field)227   static boolean isVoidField(Field field) {
228     return field.getType().equals(Void.class);
229   }
230 
231   /** Returns whether the arg is an expansion option. */
isExpansionOption(Option annotation)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    */
usesExpansionFunction(Option annotation)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    */
findConverter(Field optionField)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    */
getAllAnnotatedFieldsSorted( Class<? extends OptionsBase> optionsClass)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 
retrieveDefaultFromAnnotation(Field optionField)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 
checkForCollisions( Map<A, Field> aFieldMap, A optionName, String description)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 
checkForBooleanAliasCollisions( Map<String, String> booleanAliasMap, String optionName, String description)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 
checkAndUpdateBooleanAliases( Map<String, Field> nameToFieldMap, Map<String, String> booleanAliasMap, String optionName)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    */
from(Collection<Class<? extends OptionsBase>> classes)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