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
15package com.google.devtools.common.options;
16
17import com.google.common.collect.ImmutableList;
18import com.google.common.collect.ImmutableMap;
19import com.google.common.collect.Ordering;
20import com.google.devtools.common.options.OptionsParser.ConstructionException;
21import java.lang.reflect.Constructor;
22import java.lang.reflect.Field;
23import java.lang.reflect.Method;
24import java.lang.reflect.Modifier;
25import java.lang.reflect.ParameterizedType;
26import java.lang.reflect.Type;
27import java.util.ArrayList;
28import java.util.Collection;
29import java.util.Collections;
30import java.util.HashMap;
31import java.util.LinkedHashMap;
32import java.util.List;
33import java.util.Map;
34import 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
51public 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