• 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.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). */
getAllOptionDefinitionsForClass( Class<? extends OptionsBase> optionsClass)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