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.Ordering; 20 import com.google.devtools.common.options.OptionsParser.ConstructionException; 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.ArrayList; 28 import java.util.Collection; 29 import java.util.Collections; 30 import java.util.HashMap; 31 import java.util.LinkedHashMap; 32 import java.util.List; 33 import java.util.Map; 34 import 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 51 public 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 IsolatedOptionsData( Map<Class<? extends OptionsBase>, Constructor<?>> optionsClasses, Map<String, Field> nameToField, Map<Character, Field> abbrevToField, Map<Class<? extends OptionsBase>, ImmutableList<Field>> allOptionsFields, Map<Field, Object> optionDefaults, Map<Field, Converter<?>> converters, Map<Field, Boolean> allowMultiple, Map<Class<? extends OptionsBase>, Boolean> usesOnlyCoreTypes)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 IsolatedOptionsData(IsolatedOptionsData other)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 */ getOptionsClasses()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. getConstructor(Class<T> clazz)149 public <T extends OptionsBase> Constructor<T> getConstructor(Class<T> clazz) { 150 return (Constructor<T>) optionsClasses.get(clazz); 151 } 152 getFieldFromName(String name)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 */ getAllNamedFields()162 public Iterable<Map.Entry<String, Field>> getAllNamedFields() { 163 return nameToField.entrySet(); 164 } 165 getFieldForAbbrev(char abbrev)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 */ getFieldsForClass(Class<? extends OptionsBase> optionsClass)174 public ImmutableList<Field> getFieldsForClass(Class<? extends OptionsBase> optionsClass) { 175 return allOptionsFields.get(optionsClass); 176 } 177 getDefaultValue(Field field)178 public Object getDefaultValue(Field field) { 179 return optionDefaults.get(field); 180 } 181 getConverter(Field field)182 public Converter<?> getConverter(Field field) { 183 return converters.get(field); 184 } 185 getAllowMultiple(Field field)186 public boolean getAllowMultiple(Field field) { 187 return allowMultiple.get(field); 188 } 189 getUsesOnlyCoreTypes(Class<? extends OptionsBase> optionsClass)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 */ getFieldSingularType(Field field, Option annotation)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 */ isBooleanField(Field field)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. */ isVoidField(Field field)227 static boolean isVoidField(Field field) { 228 return field.getType().equals(Void.class); 229 } 230 231 /** Returns whether the arg is an expansion option. */ isExpansionOption(Option annotation)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 */ usesExpansionFunction(Option annotation)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 */ findConverter(Field optionField)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 */ getAllAnnotatedFieldsSorted( Class<? extends OptionsBase> optionsClass)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 retrieveDefaultFromAnnotation(Field optionField)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 checkForCollisions( Map<A, Field> aFieldMap, A optionName, String description)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 checkForBooleanAliasCollisions( Map<String, String> booleanAliasMap, String optionName, String description)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 checkAndUpdateBooleanAliases( Map<String, Field> nameToFieldMap, Map<String, String> booleanAliasMap, String optionName)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 */ from(Collection<Class<? extends OptionsBase>> classes)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