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.Lists;
20 import com.google.common.collect.Maps;
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.Collection;
28 import java.util.Collections;
29 import java.util.List;
30 import java.util.Map;
31 import javax.annotation.concurrent.Immutable;
32 
33 /**
34  * An immutable selection of options data corresponding to a set of options classes. The data is
35  * collected using reflection, which can be expensive. Therefore this class can be used internally
36  * to cache the results.
37  *
38  * <p>The data is isolated in the sense that it has not yet been processed to add inter-option-
39  * dependent information -- namely, the results of evaluating expansion functions. The {@link
40  * OptionsData} subclass stores this added information. The reason for the split is so that we can
41  * avoid exposing to expansion functions the effects of evaluating other expansion functions, to
42  * ensure that the order in which they run is not significant.
43  */
44 // TODO(brandjon): This class is technically not necessarily immutable due to optionsDefault
45 // accepting Object values, and the List in allOptionsField should be ImmutableList. Either fix
46 // this or remove @Immutable.
47 @Immutable
48 class IsolatedOptionsData extends OpaqueOptionsData {
49 
50   /**
51    * These are the options-declaring classes which are annotated with {@link Option} annotations.
52    */
53   private final ImmutableMap<Class<? extends OptionsBase>, Constructor<?>> optionsClasses;
54 
55   /** Maps option name to Option-annotated Field. */
56   private final ImmutableMap<String, Field> nameToField;
57 
58   /** Maps option abbreviation to Option-annotated Field. */
59   private final ImmutableMap<Character, Field> abbrevToField;
60 
61   /** For each options class, contains a list of all Option-annotated fields in that class. */
62   private final ImmutableMap<Class<? extends OptionsBase>, List<Field>> allOptionsFields;
63 
64   /** Mapping from each Option-annotated field to the default value for that field. */
65   // Immutable like the others, but uses Collections.unmodifiableMap because of null values.
66   private final Map<Field, Object> optionDefaults;
67 
68   /**
69    * Mapping from each Option-annotated field to the proper converter.
70    *
71    * @see #findConverter
72    */
73   private final ImmutableMap<Field, Converter<?>> converters;
74 
75   /**
76    * Mapping from each Option-annotated field to a boolean for whether that field allows multiple
77    * values.
78    */
79   private final ImmutableMap<Field, Boolean> allowMultiple;
80 
IsolatedOptionsData( Map<Class<? extends OptionsBase>, Constructor<?>> optionsClasses, Map<String, Field> nameToField, Map<Character, Field> abbrevToField, Map<Class<? extends OptionsBase>, List<Field>> allOptionsFields, Map<Field, Object> optionDefaults, Map<Field, Converter<?>> converters, Map<Field, Boolean> allowMultiple)81   private IsolatedOptionsData(
82       Map<Class<? extends OptionsBase>, Constructor<?>> optionsClasses,
83       Map<String, Field> nameToField,
84       Map<Character, Field> abbrevToField,
85       Map<Class<? extends OptionsBase>, List<Field>> allOptionsFields,
86       Map<Field, Object> optionDefaults,
87       Map<Field, Converter<?>> converters,
88       Map<Field, Boolean> allowMultiple) {
89     this.optionsClasses = ImmutableMap.copyOf(optionsClasses);
90     this.nameToField = ImmutableMap.copyOf(nameToField);
91     this.abbrevToField = ImmutableMap.copyOf(abbrevToField);
92     this.allOptionsFields = ImmutableMap.copyOf(allOptionsFields);
93     // Can't use an ImmutableMap here because of null values.
94     this.optionDefaults = Collections.unmodifiableMap(optionDefaults);
95     this.converters = ImmutableMap.copyOf(converters);
96     this.allowMultiple = ImmutableMap.copyOf(allowMultiple);
97   }
98 
IsolatedOptionsData(IsolatedOptionsData other)99   protected IsolatedOptionsData(IsolatedOptionsData other) {
100     this(
101         other.optionsClasses,
102         other.nameToField,
103         other.abbrevToField,
104         other.allOptionsFields,
105         other.optionDefaults,
106         other.converters,
107         other.allowMultiple);
108   }
109 
getOptionsClasses()110   public Collection<Class<? extends OptionsBase>> getOptionsClasses() {
111     return optionsClasses.keySet();
112   }
113 
114   @SuppressWarnings("unchecked") // The construction ensures that the case is always valid.
getConstructor(Class<T> clazz)115   public <T extends OptionsBase> Constructor<T> getConstructor(Class<T> clazz) {
116     return (Constructor<T>) optionsClasses.get(clazz);
117   }
118 
getFieldFromName(String name)119   public Field getFieldFromName(String name) {
120     return nameToField.get(name);
121   }
122 
getAllNamedFields()123   public Iterable<Map.Entry<String, Field>> getAllNamedFields() {
124     return nameToField.entrySet();
125   }
126 
getFieldForAbbrev(char abbrev)127   public Field getFieldForAbbrev(char abbrev) {
128     return abbrevToField.get(abbrev);
129   }
130 
getFieldsForClass(Class<? extends OptionsBase> optionsClass)131   public List<Field> getFieldsForClass(Class<? extends OptionsBase> optionsClass) {
132     return allOptionsFields.get(optionsClass);
133   }
134 
getDefaultValue(Field field)135   public Object getDefaultValue(Field field) {
136     return optionDefaults.get(field);
137   }
138 
getConverter(Field field)139   public Converter<?> getConverter(Field field) {
140     return converters.get(field);
141   }
142 
getAllowMultiple(Field field)143   public boolean getAllowMultiple(Field field) {
144     return allowMultiple.get(field);
145   }
146 
147   /**
148    * For an option that does not use {@link Option#allowMultiple}, returns its type. For an option
149    * that does use it, asserts that the type is a {@code List<T>} and returns its element type
150    * {@code T}.
151    */
getFieldSingularType(Field field, Option annotation)152   private static Type getFieldSingularType(Field field, Option annotation) {
153     Type fieldType = field.getGenericType();
154     if (annotation.allowMultiple()) {
155       // If the type isn't a List<T>, this is an error in the option's declaration.
156       if (!(fieldType instanceof ParameterizedType)) {
157         throw new AssertionError("Type of multiple occurrence option must be a List<...>");
158       }
159       ParameterizedType pfieldType = (ParameterizedType) fieldType;
160       if (pfieldType.getRawType() != List.class) {
161         throw new AssertionError("Type of multiple occurrence option must be a List<...>");
162       }
163       fieldType = pfieldType.getActualTypeArguments()[0];
164     }
165     return fieldType;
166   }
167 
168   /**
169    * Returns whether a field should be considered as boolean.
170    *
171    * <p>Can be used for usage help and controlling whether the "no" prefix is allowed.
172    */
isBooleanField(Field field)173   static boolean isBooleanField(Field field) {
174     return field.getType().equals(boolean.class)
175         || field.getType().equals(TriState.class)
176         || findConverter(field) instanceof BoolOrEnumConverter;
177   }
178 
179   /** Returns whether a field has Void type. */
isVoidField(Field field)180   static boolean isVoidField(Field field) {
181     return field.getType().equals(Void.class);
182   }
183 
184   /**
185    * Returns whether the arg is an expansion option defined by an expansion function (and not a
186    * constant expansion value).
187    */
usesExpansionFunction(Option annotation)188   static boolean usesExpansionFunction(Option annotation) {
189     return annotation.expansionFunction() != ExpansionFunction.class;
190   }
191 
192   /**
193    * Given an {@code @Option}-annotated field, retrieves the {@link Converter} that will be used,
194    * taking into account the default converters if an explicit one is not specified.
195    */
findConverter(Field optionField)196   static Converter<?> findConverter(Field optionField) {
197     Option annotation = optionField.getAnnotation(Option.class);
198     if (annotation.converter() == Converter.class) {
199       // No converter provided, use the default one.
200       Type type = getFieldSingularType(optionField, annotation);
201       Converter<?> converter = Converters.DEFAULT_CONVERTERS.get(type);
202       if (converter == null) {
203         throw new AssertionError(
204             "No converter found for "
205                 + type
206                 + "; possible fix: add "
207                 + "converter=... to @Option annotation for "
208                 + optionField.getName());
209       }
210       return converter;
211     }
212     try {
213       // Instantiate the given Converter class.
214       Class<?> converter = annotation.converter();
215       Constructor<?> constructor = converter.getConstructor();
216       return (Converter<?>) constructor.newInstance();
217     } catch (Exception e) {
218       // This indicates an error in the Converter, and should be discovered the first time it is
219       // used.
220       throw new AssertionError(e);
221     }
222   }
223 
getAllAnnotatedFields(Class<? extends OptionsBase> optionsClass)224   private static List<Field> getAllAnnotatedFields(Class<? extends OptionsBase> optionsClass) {
225     List<Field> allFields = Lists.newArrayList();
226     for (Field field : optionsClass.getFields()) {
227       if (field.isAnnotationPresent(Option.class)) {
228         allFields.add(field);
229       }
230     }
231     if (allFields.isEmpty()) {
232       throw new IllegalStateException(optionsClass + " has no public @Option-annotated fields");
233     }
234     return ImmutableList.copyOf(allFields);
235   }
236 
retrieveDefaultFromAnnotation(Field optionField)237   private static Object retrieveDefaultFromAnnotation(Field optionField) {
238     Converter<?> converter = findConverter(optionField);
239     String defaultValueAsString = OptionsParserImpl.getDefaultOptionString(optionField);
240     // Special case for "null"
241     if (OptionsParserImpl.isSpecialNullDefault(defaultValueAsString, optionField)) {
242       return null;
243     }
244     boolean allowsMultiple = optionField.getAnnotation(Option.class).allowMultiple();
245     // If the option allows multiple values then we intentionally return the empty list as
246     // the default value of this option since it is not always the case that an option
247     // that allows multiple values will have a converter that returns a list value.
248     if (allowsMultiple) {
249       return Collections.emptyList();
250     }
251     // Otherwise try to convert the default value using the converter
252     Object convertedValue;
253     try {
254       convertedValue = converter.convert(defaultValueAsString);
255     } catch (OptionsParsingException e) {
256       throw new IllegalStateException("OptionsParsingException while "
257           + "retrieving default for " + optionField.getName() + ": "
258           + e.getMessage());
259     }
260     return convertedValue;
261   }
262 
263   /**
264    * Constructs an {@link IsolatedOptionsData} object for a parser that knows about the given
265    * {@link OptionsBase} classes. No inter-option analysis is done. Performs basic sanity checking
266    * on each option in isolation.
267    */
from(Collection<Class<? extends OptionsBase>> classes)268   static IsolatedOptionsData from(Collection<Class<? extends OptionsBase>> classes) {
269     Map<Class<? extends OptionsBase>, Constructor<?>> constructorBuilder = Maps.newHashMap();
270     Map<Class<? extends OptionsBase>, List<Field>> allOptionsFieldsBuilder = Maps.newHashMap();
271     Map<String, Field> nameToFieldBuilder = Maps.newHashMap();
272     Map<Character, Field> abbrevToFieldBuilder = Maps.newHashMap();
273     Map<Field, Object> optionDefaultsBuilder = Maps.newHashMap();
274     Map<Field, Converter<?>> convertersBuilder = Maps.newHashMap();
275     Map<Field, Boolean> allowMultipleBuilder = Maps.newHashMap();
276 
277     // Read all Option annotations:
278     for (Class<? extends OptionsBase> parsedOptionsClass : classes) {
279       try {
280         Constructor<? extends OptionsBase> constructor =
281             parsedOptionsClass.getConstructor();
282         constructorBuilder.put(parsedOptionsClass, constructor);
283       } catch (NoSuchMethodException e) {
284         throw new IllegalArgumentException(parsedOptionsClass
285             + " lacks an accessible default constructor");
286       }
287       List<Field> fields = getAllAnnotatedFields(parsedOptionsClass);
288       allOptionsFieldsBuilder.put(parsedOptionsClass, fields);
289 
290       for (Field field : fields) {
291         Option annotation = field.getAnnotation(Option.class);
292 
293         if (annotation.name() == null) {
294           throw new AssertionError("Option cannot have a null name");
295         }
296 
297         Type fieldType = getFieldSingularType(field, annotation);
298 
299         // Get the converter return type.
300         @SuppressWarnings("rawtypes")
301         Class<? extends Converter> converter = annotation.converter();
302         if (converter == Converter.class) {
303           Converter<?> actualConverter = Converters.DEFAULT_CONVERTERS.get(fieldType);
304           if (actualConverter == null) {
305             throw new AssertionError("Cannot find converter for field of type "
306                 + field.getType() + " named " + field.getName()
307                 + " in class " + field.getDeclaringClass().getName());
308           }
309           converter = actualConverter.getClass();
310         }
311         if (Modifier.isAbstract(converter.getModifiers())) {
312           throw new AssertionError("The converter type " + converter
313               + " must be a concrete type");
314         }
315         Type converterResultType;
316         try {
317           Method convertMethod = converter.getMethod("convert", String.class);
318           converterResultType = GenericTypeHelper.getActualReturnType(converter, convertMethod);
319         } catch (NoSuchMethodException e) {
320           throw new AssertionError("A known converter object doesn't implement the convert"
321               + " method");
322         }
323 
324         if (annotation.allowMultiple()) {
325           if (GenericTypeHelper.getRawType(converterResultType) == List.class) {
326             Type elementType =
327                 ((ParameterizedType) converterResultType).getActualTypeArguments()[0];
328             if (!GenericTypeHelper.isAssignableFrom(fieldType, elementType)) {
329               throw new AssertionError("If the converter return type of a multiple occurance " +
330                   "option is a list, then the type of list elements (" + fieldType + ") must be " +
331                   "assignable from the converter list element type (" + elementType + ")");
332             }
333           } else {
334             if (!GenericTypeHelper.isAssignableFrom(fieldType, converterResultType)) {
335               throw new AssertionError("Type of list elements (" + fieldType +
336                   ") for multiple occurrence option must be assignable from the converter " +
337                   "return type (" + converterResultType + ")");
338             }
339           }
340         } else {
341           if (!GenericTypeHelper.isAssignableFrom(fieldType, converterResultType)) {
342             throw new AssertionError("Type of field (" + fieldType +
343                 ") must be assignable from the converter " +
344                 "return type (" + converterResultType + ")");
345           }
346         }
347 
348         if (nameToFieldBuilder.put(annotation.name(), field) != null) {
349           throw new DuplicateOptionDeclarationException(
350               "Duplicate option name: --" + annotation.name());
351         }
352         if (!annotation.oldName().isEmpty()) {
353           if (nameToFieldBuilder.put(annotation.oldName(), field) != null) {
354             throw new DuplicateOptionDeclarationException(
355                 "Old option name duplicates option name: --" + annotation.oldName());
356           }
357         }
358         if (annotation.abbrev() != '\0') {
359           if (abbrevToFieldBuilder.put(annotation.abbrev(), field) != null) {
360             throw new DuplicateOptionDeclarationException(
361                   "Duplicate option abbrev: -" + annotation.abbrev());
362           }
363         }
364 
365         optionDefaultsBuilder.put(field, retrieveDefaultFromAnnotation(field));
366 
367         convertersBuilder.put(field, findConverter(field));
368 
369         allowMultipleBuilder.put(field, annotation.allowMultiple());
370       }
371     }
372 
373     return new IsolatedOptionsData(
374         constructorBuilder,
375         nameToFieldBuilder,
376         abbrevToFieldBuilder,
377         allOptionsFieldsBuilder,
378         optionDefaultsBuilder,
379         convertersBuilder,
380         allowMultipleBuilder);
381   }
382 }
383