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.devtools.common.options.OptionDefinition.NotAnOptionException;
     20 import com.google.devtools.common.options.OptionsParser.ConstructionException;
     21 import java.lang.reflect.Constructor;
     22 import java.util.Arrays;
     23 import java.util.Collection;
     24 import java.util.HashMap;
     25 import java.util.LinkedHashMap;
     26 import java.util.Map;
     27 import java.util.Objects;
     28 import javax.annotation.concurrent.Immutable;
     29 
     30 /**
     31  * A selection of options data corresponding to a set of {@link OptionsBase} subclasses (options
     32  * classes). The data is collected using reflection, which can be expensive. Therefore this class
     33  * can be used internally to cache the results.
     34  *
     35  * <p>The data is isolated in the sense that it has not yet been processed to add
     36  * inter-option-dependent information -- namely, the results of evaluating expansion functions. The
     37  * {@link OptionsData} subclass stores this added information. The reason for the split is so that
     38  * we can avoid exposing to expansion functions the effects of evaluating other expansion functions,
     39  * to ensure that the order in which they run is not significant.
     40  *
     41  * <p>This class is immutable so long as the converters and default values associated with the
     42  * options are immutable.
     43  */
     44 @Immutable
     45 public class IsolatedOptionsData extends OpaqueOptionsData {
     46 
     47   /**
     48    * Cache for the options in an OptionsBase.
     49    *
     50    * <p>Mapping from options class to a list of all {@code OptionFields} in that class. The map
     51    * entries are unordered, but the fields in the lists are ordered alphabetically. This caches the
     52    * work of reflection done for the same {@code optionsBase} across multiple {@link OptionsData}
     53    * instances, and must be used through the thread safe {@link
     54    * #getAllOptionDefinitionsForClass(Class)}
     55    */
     56   private static final Map<Class<? extends OptionsBase>, ImmutableList<OptionDefinition>>
     57       allOptionsFields = new HashMap<>();
     58 
     59   /** Returns all {@code optionDefinitions}, ordered by their option name (not their field name). */
     60   public static synchronized ImmutableList<OptionDefinition> getAllOptionDefinitionsForClass(
     61       Class<? extends OptionsBase> optionsClass) {
     62     return allOptionsFields.computeIfAbsent(
     63         optionsClass,
     64         optionsBaseClass ->
     65             Arrays.stream(optionsBaseClass.getFields())
     66                 .map(
     67                     field -> {
     68                       try {
     69                         return OptionDefinition.extractOptionDefinition(field);
     70                       } catch (NotAnOptionException e) {
     71                         // Ignore non-@Option annotated fields. Requiring all fields in the
     72                         // OptionsBase to be @Option-annotated requires a depot cleanup.
     73                         return null;
     74                       }
     75                     })
     76                 .filter(Objects::nonNull)
     77                 .sorted(OptionDefinition.BY_OPTION_NAME)
     78                 .collect(ImmutableList.toImmutableList()));
     79   }
     80 
     81   /**
     82    * Mapping from each options class to its no-arg constructor. Entries appear in the same order
     83    * that they were passed to {@link #from(Collection)}.
     84    */
     85   private final ImmutableMap<Class<? extends OptionsBase>, Constructor<?>> optionsClasses;
     86 
     87   /**
     88    * Mapping from option name to {@code OptionDefinition}. Entries appear ordered first by their
     89    * options class (the order in which they were passed to {@link #from(Collection)}, and then in
     90    * alphabetic order within each options class.
     91    */
     92   private final ImmutableMap<String, OptionDefinition> nameToField;
     93 
     94   /**
     95    * For options that have an "OldName", this is a mapping from old name to its corresponding {@code
     96    * OptionDefinition}. Entries appear ordered first by their options class (the order in which they
     97    * were passed to {@link #from(Collection)}, and then in alphabetic order within each options
     98    * class.
     99    */
    100   private final ImmutableMap<String, OptionDefinition> oldNameToField;
    101 
    102   /** Mapping from option abbreviation to {@code OptionDefinition} (unordered). */
    103   private final ImmutableMap<Character, OptionDefinition> abbrevToField;
    104 
    105 
    106   /**
    107    * Mapping from each options class to whether or not it has the {@link UsesOnlyCoreTypes}
    108    * annotation (unordered).
    109    */
    110   private final ImmutableMap<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypes;
    111 
    112   private IsolatedOptionsData(
    113       Map<Class<? extends OptionsBase>, Constructor<?>> optionsClasses,
    114       Map<String, OptionDefinition> nameToField,
    115       Map<String, OptionDefinition> oldNameToField,
    116       Map<Character, OptionDefinition> abbrevToField,
    117       Map<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypes) {
    118     this.optionsClasses = ImmutableMap.copyOf(optionsClasses);
    119     this.nameToField = ImmutableMap.copyOf(nameToField);
    120     this.oldNameToField = ImmutableMap.copyOf(oldNameToField);
    121     this.abbrevToField = ImmutableMap.copyOf(abbrevToField);
    122     this.usesOnlyCoreTypes = ImmutableMap.copyOf(usesOnlyCoreTypes);
    123   }
    124 
    125   protected IsolatedOptionsData(IsolatedOptionsData other) {
    126     this(
    127         other.optionsClasses,
    128         other.nameToField,
    129         other.oldNameToField,
    130         other.abbrevToField,
    131         other.usesOnlyCoreTypes);
    132   }
    133 
    134   /**
    135    * Returns all options classes indexed by this options data object, in the order they were passed
    136    * to {@link #from(Collection)}.
    137    */
    138   public Collection<Class<? extends OptionsBase>> getOptionsClasses() {
    139     return optionsClasses.keySet();
    140   }
    141 
    142   @SuppressWarnings("unchecked") // The construction ensures that the case is always valid.
    143   public <T extends OptionsBase> Constructor<T> getConstructor(Class<T> clazz) {
    144     return (Constructor<T>) optionsClasses.get(clazz);
    145   }
    146 
    147   /**
    148    * Returns the option in this parser by the provided name, or {@code null} if none is found. This
    149    * will match both the canonical name of an option, and any old name listed that we still accept.
    150    */
    151   public OptionDefinition getOptionDefinitionFromName(String name) {
    152     return nameToField.getOrDefault(name, oldNameToField.get(name));
    153   }
    154 
    155   /**
    156    * Returns all {@link OptionDefinition} objects loaded, mapped by their canonical names. Entries
    157    * appear ordered first by their options class (the order in which they were passed to {@link
    158    * #from(Collection)}, and then in alphabetic order within each options class.
    159    */
    160   public Iterable<Map.Entry<String, OptionDefinition>> getAllOptionDefinitions() {
    161     return nameToField.entrySet();
    162   }
    163 
    164   public OptionDefinition getFieldForAbbrev(char abbrev) {
    165     return abbrevToField.get(abbrev);
    166   }
    167 
    168   public boolean getUsesOnlyCoreTypes(Class<? extends OptionsBase> optionsClass) {
    169     return usesOnlyCoreTypes.get(optionsClass);
    170   }
    171 
    172   /**
    173    * Generic method to check for collisions between the names we give options. Useful for checking
    174    * both single-character abbreviations and full names.
    175    */
    176   private static <A> void checkForCollisions(
    177       Map<A, OptionDefinition> aFieldMap, A optionName, String description)
    178       throws DuplicateOptionDeclarationException {
    179     if (aFieldMap.containsKey(optionName)) {
    180       throw new DuplicateOptionDeclarationException(
    181           "Duplicate option name, due to " + description + ": --" + optionName);
    182     }
    183   }
    184 
    185   /**
    186    * All options, even non-boolean ones, should check that they do not conflict with previously
    187    * loaded boolean options.
    188    */
    189   private static void checkForBooleanAliasCollisions(
    190       Map<String, String> booleanAliasMap, String optionName, String description)
    191       throws DuplicateOptionDeclarationException {
    192     if (booleanAliasMap.containsKey(optionName)) {
    193       throw new DuplicateOptionDeclarationException(
    194           "Duplicate option name, due to "
    195               + description
    196               + " --"
    197               + optionName
    198               + ", it conflicts with a negating alias for boolean flag --"
    199               + booleanAliasMap.get(optionName));
    200     }
    201   }
    202 
    203   /**
    204    * For an {@code option} of boolean type, this checks that the boolean alias does not conflict
    205    * with other names, and adds the boolean alias to a list so that future flags can find if they
    206    * conflict with a boolean alias..
    207    */
    208   private static void checkAndUpdateBooleanAliases(
    209       Map<String, OptionDefinition> nameToFieldMap,
    210       Map<String, OptionDefinition> oldNameToFieldMap,
    211       Map<String, String> booleanAliasMap,
    212       String optionName)
    213       throws DuplicateOptionDeclarationException {
    214     // Check that the negating alias does not conflict with existing flags.
    215     checkForCollisions(nameToFieldMap, "no" + optionName, "boolean option alias");
    216     checkForCollisions(oldNameToFieldMap, "no" + optionName, "boolean option alias");
    217 
    218     // Record that the boolean option takes up additional namespace for its negating alias.
    219     booleanAliasMap.put("no" + optionName, optionName);
    220   }
    221 
    222   /**
    223    * Constructs an {@link IsolatedOptionsData} object for a parser that knows about the given
    224    * {@link OptionsBase} classes. No inter-option analysis is done. Performs basic sanity checking
    225    * on each option in isolation.
    226    */
    227   static IsolatedOptionsData from(Collection<Class<? extends OptionsBase>> classes) {
    228     // Mind which fields have to preserve order.
    229     Map<Class<? extends OptionsBase>, Constructor<?>> constructorBuilder = new LinkedHashMap<>();
    230     Map<String, OptionDefinition> nameToFieldBuilder = new LinkedHashMap<>();
    231     Map<String, OptionDefinition> oldNameToFieldBuilder = new LinkedHashMap<>();
    232     Map<Character, OptionDefinition> abbrevToFieldBuilder = new HashMap<>();
    233 
    234     // Maps the negated boolean flag aliases to the original option name.
    235     Map<String, String> booleanAliasMap = new HashMap<>();
    236 
    237     Map<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypesBuilder = new HashMap<>();
    238 
    239     // Combine the option definitions for these options classes, and check that they do not
    240     // conflict. The options are individually checked for correctness at compile time in the
    241     // OptionProcessor.
    242     for (Class<? extends OptionsBase> parsedOptionsClass : classes) {
    243       try {
    244         Constructor<? extends OptionsBase> constructor = parsedOptionsClass.getConstructor();
    245         constructorBuilder.put(parsedOptionsClass, constructor);
    246       } catch (NoSuchMethodException e) {
    247         throw new IllegalArgumentException(parsedOptionsClass
    248             + " lacks an accessible default constructor");
    249       }
    250       ImmutableList<OptionDefinition> optionDefinitions =
    251           getAllOptionDefinitionsForClass(parsedOptionsClass);
    252 
    253       for (OptionDefinition optionDefinition : optionDefinitions) {
    254         try {
    255           String optionName = optionDefinition.getOptionName();
    256           checkForCollisions(nameToFieldBuilder, optionName, "option name collision");
    257           checkForCollisions(
    258               oldNameToFieldBuilder,
    259               optionName,
    260               "option name collision with another option's old name");
    261           checkForBooleanAliasCollisions(booleanAliasMap, optionName, "option");
    262           if (optionDefinition.usesBooleanValueSyntax()) {
    263             checkAndUpdateBooleanAliases(
    264                 nameToFieldBuilder, oldNameToFieldBuilder, booleanAliasMap, optionName);
    265           }
    266           nameToFieldBuilder.put(optionName, optionDefinition);
    267 
    268           if (!optionDefinition.getOldOptionName().isEmpty()) {
    269             String oldName = optionDefinition.getOldOptionName();
    270             checkForCollisions(
    271                 nameToFieldBuilder,
    272                 oldName,
    273                 "old option name collision with another option's canonical name");
    274             checkForCollisions(
    275                 oldNameToFieldBuilder,
    276                 oldName,
    277                 "old option name collision with another old option name");
    278             checkForBooleanAliasCollisions(booleanAliasMap, oldName, "old option name");
    279             // If boolean, repeat the alias dance for the old name.
    280             if (optionDefinition.usesBooleanValueSyntax()) {
    281               checkAndUpdateBooleanAliases(
    282                   nameToFieldBuilder, oldNameToFieldBuilder, booleanAliasMap, oldName);
    283             }
    284             // Now that we've checked for conflicts, confidently store the old name.
    285             oldNameToFieldBuilder.put(oldName, optionDefinition);
    286           }
    287           if (optionDefinition.getAbbreviation() != '\0') {
    288             checkForCollisions(
    289                 abbrevToFieldBuilder, optionDefinition.getAbbreviation(), "option abbreviation");
    290             abbrevToFieldBuilder.put(optionDefinition.getAbbreviation(), optionDefinition);
    291           }
    292         } catch (DuplicateOptionDeclarationException e) {
    293           throw new ConstructionException(e);
    294         }
    295       }
    296 
    297       boolean usesOnlyCoreTypes = parsedOptionsClass.isAnnotationPresent(UsesOnlyCoreTypes.class);
    298       if (usesOnlyCoreTypes) {
    299         // Validate that @UsesOnlyCoreTypes was used correctly.
    300         for (OptionDefinition optionDefinition : optionDefinitions) {
    301           // The classes in coreTypes are all final. But even if they weren't, we only want to check
    302           // for exact matches; subclasses would not be considered core types.
    303           if (!UsesOnlyCoreTypes.CORE_TYPES.contains(optionDefinition.getType())) {
    304             throw new ConstructionException(
    305                 "Options class '"
    306                     + parsedOptionsClass.getName()
    307                     + "' is marked as "
    308                     + "@UsesOnlyCoreTypes, but field '"
    309                     + optionDefinition.getField().getName()
    310                     + "' has type '"
    311                     + optionDefinition.getType().getName()
    312                     + "'");
    313           }
    314         }
    315       }
    316       usesOnlyCoreTypesBuilder.put(parsedOptionsClass, usesOnlyCoreTypes);
    317     }
    318 
    319     return new IsolatedOptionsData(
    320         constructorBuilder,
    321         nameToFieldBuilder,
    322         oldNameToFieldBuilder,
    323         abbrevToFieldBuilder,
    324         usesOnlyCoreTypesBuilder);
    325   }
    326 
    327 }
    328