blob: 29e3e29952d473fd34d8d73715dd4b9b2c427cec [file] [log] [blame]
ccalvarine8aae032017-08-22 07:17:44 +02001// 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
15package com.google.devtools.common.options;
16
17import com.google.devtools.common.options.OptionsParser.ConstructionException;
ccalvarin00443492017-08-30 00:23:40 +020018import java.lang.reflect.Constructor;
ccalvarine8aae032017-08-22 07:17:44 +020019import java.lang.reflect.Field;
ccalvarin00443492017-08-30 00:23:40 +020020import java.lang.reflect.ParameterizedType;
21import java.lang.reflect.Type;
22import java.util.Collections;
ccalvarine8aae032017-08-22 07:17:44 +020023import 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 */
ccalvarinc50cd132017-10-30 07:04:07 -040032public class OptionDefinition implements Comparable<OptionDefinition> {
ccalvarin00443492017-08-30 00:23:40 +020033
34 // TODO(b/65049598) make ConstructionException checked, which will make this checked as well.
ccalvarin987f09f2017-08-31 19:50:39 +020035 static class NotAnOptionException extends ConstructionException {
36 NotAnOptionException(Field field) {
ccalvarin00443492017-08-30 00:23:40 +020037 super(
ccalvarin3e44d5b2017-08-31 06:32:03 +020038 "The field "
39 + field.getName()
40 + " does not have the right annotation to be considered an option.");
ccalvarin00443492017-08-30 00:23:40 +020041 }
42 }
ccalvarine8aae032017-08-22 07:17:44 +020043
44 /**
45 * If the {@code field} is annotated with the appropriate @{@link Option} annotation, returns the
ccalvarin00443492017-08-30 00:23:40 +020046 * {@code OptionDefinition} for that option. Otherwise, throws a {@link NotAnOptionException}.
ccalvarin987f09f2017-08-31 19:50:39 +020047 *
48 * <p>These values are cached in the {@link OptionsData} layer and should be accessed through
49 * {@link OptionsParser#getOptionDefinitions(Class)}.
ccalvarine8aae032017-08-22 07:17:44 +020050 */
ccalvarin987f09f2017-08-31 19:50:39 +020051 static OptionDefinition extractOptionDefinition(Field field) {
ccalvarine8aae032017-08-22 07:17:44 +020052 Option annotation = field == null ? null : field.getAnnotation(Option.class);
53 if (annotation == null) {
ccalvarin00443492017-08-30 00:23:40 +020054 throw new NotAnOptionException(field);
ccalvarine8aae032017-08-22 07:17:44 +020055 }
56 return new OptionDefinition(field, annotation);
57 }
58
59 private final Field field;
60 private final Option optionAnnotation;
ccalvarin00443492017-08-30 00:23:40 +020061 private Converter<?> converter = null;
62 private Object defaultValue = null;
ccalvarine8aae032017-08-22 07:17:44 +020063
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()} ()} */
ccalvarin78a5fcf2018-03-30 08:40:44 -0700140 public Class<? extends ExpansionFunction> getExpansionFunction() {
ccalvarine8aae032017-08-22 07:17:44 +0200141 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
fwe346c8ff2017-09-14 18:01:48 +0200159 /** 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
ccalvarine8aae032017-08-22 07:17:44 +0200164 /** 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
ccalvarin4acb36c2017-09-21 00:35:35 +0200183 /** Returns whether the arg is an expansion option. */
184 public boolean hasImplicitRequirements() {
185 return (getImplicitRequirements().length > 0);
186 }
187
ccalvarine8aae032017-08-22 07:17:44 +0200188 /**
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
ccalvarin00443492017-08-30 00:23:40 +0200196 /**
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()) {
ccalvarin3e44d5b2017-08-31 06:32:03 +0200204 // The validity of the converter is checked at compile time. We know the type to be
205 // List<singularType>.
ccalvarin00443492017-08-30 00:23:40 +0200206 ParameterizedType pfieldType = (ParameterizedType) fieldType;
ccalvarin00443492017-08-30 00:23:40 +0200207 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 */
ccalvarin5fe8e662017-09-14 15:56:43 +0200218 public Converter<?> getConverter() {
ccalvarin00443492017-08-30 00:23:40 +0200219 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);
ccalvarin00443492017-08-30 00:23:40 +0200227 } 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 */
ccalvarin5fe8e662017-09-14 15:56:43 +0200248 public boolean usesBooleanValueSyntax() {
ccalvarin00443492017-08-30 00:23:40 +0200249 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() {
Googler3d2d8b62020-03-16 08:28:33 -0700256 if (defaultValue != null) {
ccalvarin00443492017-08-30 00:23:40 +0200257 return defaultValue;
258 }
ccalvarin00443492017-08-30 00:23:40 +0200259 // 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.
Googler3d2d8b62020-03-16 08:28:33 -0700262 if (allowsMultiple()) {
ccalvarin00443492017-08-30 00:23:40 +0200263 defaultValue = Collections.emptyList();
Googler3d2d8b62020-03-16 08:28:33 -0700264 } else if (isSpecialNullDefault()) {
265 return null;
ccalvarin00443492017-08-30 00:23:40 +0200266 } else {
267 // Otherwise try to convert the default value using the converter
Googler3d2d8b62020-03-16 08:28:33 -0700268 Converter<?> converter = getConverter();
269 String defaultValueAsString = getUnparsedDefaultValue();
ccalvarin00443492017-08-30 00:23:40 +0200270 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
ccalvarin1dce0972017-09-11 20:03:02 +0200283 /**
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
ccalvarin7cd9e882017-10-16 22:18:32 +0200302 @Override
ccalvarinc50cd132017-10-30 07:04:07 -0400303 public int compareTo(OptionDefinition o) {
304 return getOptionName().compareTo(o.getOptionName());
305 }
306
307 @Override
ccalvarin7cd9e882017-10-16 22:18:32 +0200308 public String toString() {
309 return String.format("option '--%s'", getOptionName());
310 }
311
ccalvarine8aae032017-08-22 07:17:44 +0200312 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}