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