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