1 // Copyright 2017 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.devtools.common.options.OptionsParser.ConstructionException; 18 import java.lang.reflect.Constructor; 19 import java.lang.reflect.Field; 20 import java.lang.reflect.ParameterizedType; 21 import java.lang.reflect.Type; 22 import java.util.Collections; 23 import java.util.Comparator; 24 25 /** 26 * Everything the {@link OptionsParser} needs to know about how an option is defined. 27 * 28 * <p>An {@code OptionDefinition} is effectively a wrapper around the {@link Option} annotation and 29 * the {@link Field} that is annotated, and should contain all logic about default settings and 30 * behavior. 31 */ 32 public class OptionDefinition implements Comparable<OptionDefinition> { 33 34 // TODO(b/65049598) make ConstructionException checked, which will make this checked as well. 35 static class NotAnOptionException extends ConstructionException { NotAnOptionException(Field field)36 NotAnOptionException(Field field) { 37 super( 38 "The field " 39 + field.getName() 40 + " does not have the right annotation to be considered an option."); 41 } 42 } 43 44 /** 45 * If the {@code field} is annotated with the appropriate @{@link Option} annotation, returns the 46 * {@code OptionDefinition} for that option. Otherwise, throws a {@link NotAnOptionException}. 47 * 48 * <p>These values are cached in the {@link OptionsData} layer and should be accessed through 49 * {@link OptionsParser#getOptionDefinitions(Class)}. 50 */ extractOptionDefinition(Field field)51 static OptionDefinition extractOptionDefinition(Field field) { 52 Option annotation = field == null ? null : field.getAnnotation(Option.class); 53 if (annotation == null) { 54 throw new NotAnOptionException(field); 55 } 56 return new OptionDefinition(field, annotation); 57 } 58 59 private final Field field; 60 private final Option optionAnnotation; 61 private Converter<?> converter = null; 62 private Object defaultValue = null; 63 OptionDefinition(Field field, Option optionAnnotation)64 private OptionDefinition(Field field, Option optionAnnotation) { 65 this.field = field; 66 this.optionAnnotation = optionAnnotation; 67 } 68 69 /** Returns the underlying {@code field} for this {@code OptionDefinition}. */ getField()70 public Field getField() { 71 return field; 72 } 73 74 /** 75 * Returns the name of the option ("--name"). 76 * 77 * <p>Labelled "Option" name to distinguish it from the field's name. 78 */ getOptionName()79 public String getOptionName() { 80 return optionAnnotation.name(); 81 } 82 83 /** The single-character abbreviation of the option ("-a"). */ getAbbreviation()84 public char getAbbreviation() { 85 return optionAnnotation.abbrev(); 86 } 87 88 /** {@link Option#help()} */ getHelpText()89 public String getHelpText() { 90 return optionAnnotation.help(); 91 } 92 93 /** {@link Option#valueHelp()} */ getValueTypeHelpText()94 public String getValueTypeHelpText() { 95 return optionAnnotation.valueHelp(); 96 } 97 98 /** {@link Option#defaultValue()} */ getUnparsedDefaultValue()99 public String getUnparsedDefaultValue() { 100 return optionAnnotation.defaultValue(); 101 } 102 103 /** {@link Option#category()} */ getOptionCategory()104 public String getOptionCategory() { 105 return optionAnnotation.category(); 106 } 107 108 /** {@link Option#documentationCategory()} */ getDocumentationCategory()109 public OptionDocumentationCategory getDocumentationCategory() { 110 return optionAnnotation.documentationCategory(); 111 } 112 113 /** {@link Option#effectTags()} */ getOptionEffectTags()114 public OptionEffectTag[] getOptionEffectTags() { 115 return optionAnnotation.effectTags(); 116 } 117 118 /** {@link Option#metadataTags()} */ getOptionMetadataTags()119 public OptionMetadataTag[] getOptionMetadataTags() { 120 return optionAnnotation.metadataTags(); 121 } 122 123 /** {@link Option#converter()} ()} */ 124 @SuppressWarnings({"rawtypes"}) getProvidedConverter()125 public Class<? extends Converter> getProvidedConverter() { 126 return optionAnnotation.converter(); 127 } 128 129 /** {@link Option#allowMultiple()} */ allowsMultiple()130 public boolean allowsMultiple() { 131 return optionAnnotation.allowMultiple(); 132 } 133 134 /** {@link Option#expansion()} */ getOptionExpansion()135 public String[] getOptionExpansion() { 136 return optionAnnotation.expansion(); 137 } 138 139 /** {@link Option#expansionFunction()} ()} */ getExpansionFunction()140 Class<? extends ExpansionFunction> getExpansionFunction() { 141 return optionAnnotation.expansionFunction(); 142 } 143 144 /** {@link Option#implicitRequirements()} ()} */ getImplicitRequirements()145 public String[] getImplicitRequirements() { 146 return optionAnnotation.implicitRequirements(); 147 } 148 149 /** {@link Option#deprecationWarning()} ()} */ getDeprecationWarning()150 public String getDeprecationWarning() { 151 return optionAnnotation.deprecationWarning(); 152 } 153 154 /** {@link Option#oldName()} ()} ()} */ getOldOptionName()155 public String getOldOptionName() { 156 return optionAnnotation.oldName(); 157 } 158 159 /** {@link Option#wrapperOption()} ()} ()} */ isWrapperOption()160 public boolean isWrapperOption() { 161 return optionAnnotation.wrapperOption(); 162 } 163 164 /** Returns whether an option --foo has a negative equivalent --nofoo. */ hasNegativeOption()165 public boolean hasNegativeOption() { 166 return getType().equals(boolean.class) || getType().equals(TriState.class); 167 } 168 169 /** The type of the optionDefinition. */ getType()170 public Class<?> getType() { 171 return field.getType(); 172 } 173 174 /** Whether this field has type Void. */ isVoidField()175 boolean isVoidField() { 176 return getType().equals(Void.class); 177 } 178 isSpecialNullDefault()179 public boolean isSpecialNullDefault() { 180 return getUnparsedDefaultValue().equals("null") && !getType().isPrimitive(); 181 } 182 183 /** Returns whether the arg is an expansion option. */ isExpansionOption()184 public boolean isExpansionOption() { 185 return (getOptionExpansion().length > 0 || usesExpansionFunction()); 186 } 187 188 /** Returns whether the arg is an expansion option. */ hasImplicitRequirements()189 public boolean hasImplicitRequirements() { 190 return (getImplicitRequirements().length > 0); 191 } 192 193 /** 194 * Returns whether the arg is an expansion option defined by an expansion function (and not a 195 * constant expansion value). 196 */ usesExpansionFunction()197 public boolean usesExpansionFunction() { 198 return getExpansionFunction() != ExpansionFunction.class; 199 } 200 201 /** 202 * For an option that does not use {@link Option#allowMultiple}, returns its type. For an option 203 * that does use it, asserts that the type is a {@code List<T>} and returns its element type 204 * {@code T}. 205 */ getFieldSingularType()206 Type getFieldSingularType() { 207 Type fieldType = getField().getGenericType(); 208 if (allowsMultiple()) { 209 // The validity of the converter is checked at compile time. We know the type to be 210 // List<singularType>. 211 ParameterizedType pfieldType = (ParameterizedType) fieldType; 212 fieldType = pfieldType.getActualTypeArguments()[0]; 213 } 214 return fieldType; 215 } 216 217 /** 218 * Retrieves the {@link Converter} that will be used for this option, taking into account the 219 * default converters if an explicit one is not specified. 220 * 221 * <p>Memoizes the converter-finding logic to avoid repeating the computation. 222 */ getConverter()223 public Converter<?> getConverter() { 224 if (converter != null) { 225 return converter; 226 } 227 Class<? extends Converter> converterClass = getProvidedConverter(); 228 if (converterClass == Converter.class) { 229 // No converter provided, use the default one. 230 Type type = getFieldSingularType(); 231 converter = Converters.DEFAULT_CONVERTERS.get(type); 232 } else { 233 try { 234 // Instantiate the given Converter class. 235 Constructor<?> constructor = converterClass.getConstructor(); 236 converter = (Converter<?>) constructor.newInstance(); 237 } catch (SecurityException | IllegalArgumentException | ReflectiveOperationException e) { 238 // This indicates an error in the Converter, and should be discovered the first time it is 239 // used. 240 throw new ConstructionException( 241 String.format("Error in the provided converter for option %s", getField().getName()), 242 e); 243 } 244 } 245 return converter; 246 } 247 248 /** 249 * Returns whether a field should be considered as boolean. 250 * 251 * <p>Can be used for usage help and controlling whether the "no" prefix is allowed. 252 */ usesBooleanValueSyntax()253 public boolean usesBooleanValueSyntax() { 254 return getType().equals(boolean.class) 255 || getType().equals(TriState.class) 256 || getConverter() instanceof BoolOrEnumConverter; 257 } 258 259 /** Returns the evaluated default value for this option & memoizes the result. */ getDefaultValue()260 public Object getDefaultValue() { 261 if (defaultValue != null || isSpecialNullDefault()) { 262 return defaultValue; 263 } 264 Converter<?> converter = getConverter(); 265 String defaultValueAsString = getUnparsedDefaultValue(); 266 boolean allowsMultiple = allowsMultiple(); 267 // If the option allows multiple values then we intentionally return the empty list as 268 // the default value of this option since it is not always the case that an option 269 // that allows multiple values will have a converter that returns a list value. 270 if (allowsMultiple) { 271 defaultValue = Collections.emptyList(); 272 } else { 273 // Otherwise try to convert the default value using the converter 274 try { 275 defaultValue = converter.convert(defaultValueAsString); 276 } catch (OptionsParsingException e) { 277 throw new ConstructionException( 278 String.format( 279 "OptionsParsingException while retrieving the default value for %s: %s", 280 getField().getName(), e.getMessage()), 281 e); 282 } 283 } 284 return defaultValue; 285 } 286 287 /** 288 * {@link OptionDefinition} is really a wrapper around a {@link Field} that caches information 289 * obtained through reflection. Checking that the fields they represent are equal is sufficient 290 * to check that two {@link OptionDefinition} objects are equal. 291 */ 292 @Override equals(Object object)293 public boolean equals(Object object) { 294 if (!(object instanceof OptionDefinition)) { 295 return false; 296 } 297 OptionDefinition otherOption = (OptionDefinition) object; 298 return field.equals(otherOption.field); 299 } 300 301 @Override hashCode()302 public int hashCode() { 303 return field.hashCode(); 304 } 305 306 @Override compareTo(OptionDefinition o)307 public int compareTo(OptionDefinition o) { 308 return getOptionName().compareTo(o.getOptionName()); 309 } 310 311 @Override toString()312 public String toString() { 313 return String.format("option '--%s'", getOptionName()); 314 } 315 316 static final Comparator<OptionDefinition> BY_OPTION_NAME = 317 Comparator.comparing(OptionDefinition::getOptionName); 318 319 /** 320 * An ordering relation for option-field fields that first groups together options of the same 321 * category, then sorts by name within the category. 322 */ 323 static final Comparator<OptionDefinition> BY_CATEGORY = 324 (left, right) -> { 325 int r = left.getOptionCategory().compareTo(right.getOptionCategory()); 326 return r == 0 ? BY_OPTION_NAME.compare(left, right) : r; 327 }; 328 } 329