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