aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/common/options/OptionDefinition.java
blob: e89234bc038b50b16e3ff6885f6dc48570a53ab4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
// Copyright 2017 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.devtools.common.options;

import com.google.devtools.common.options.OptionsParser.ConstructionException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.Comparator;

/**
 * Everything the {@link OptionsParser} needs to know about how an option is defined.
 *
 * <p>An {@code OptionDefinition} is effectively a wrapper around the {@link Option} annotation and
 * the {@link Field} that is annotated, and should contain all logic about default settings and
 * behavior.
 */
public class OptionDefinition implements Comparable<OptionDefinition> {

  // TODO(b/65049598) make ConstructionException checked, which will make this checked as well.
  static class NotAnOptionException extends ConstructionException {
    NotAnOptionException(Field field) {
      super(
          "The field "
              + field.getName()
              + " does not have the right annotation to be considered an option.");
    }
  }

  /**
   * If the {@code field} is annotated with the appropriate @{@link Option} annotation, returns the
   * {@code OptionDefinition} for that option. Otherwise, throws a {@link NotAnOptionException}.
   *
   * <p>These values are cached in the {@link OptionsData} layer and should be accessed through
   * {@link OptionsParser#getOptionDefinitions(Class)}.
   */
  static OptionDefinition extractOptionDefinition(Field field) {
    Option annotation = field == null ? null : field.getAnnotation(Option.class);
    if (annotation == null) {
      throw new NotAnOptionException(field);
    }
    return new OptionDefinition(field, annotation);
  }

  private final Field field;
  private final Option optionAnnotation;
  private Converter<?> converter = null;
  private Object defaultValue = null;

  private OptionDefinition(Field field, Option optionAnnotation) {
    this.field = field;
    this.optionAnnotation = optionAnnotation;
  }

  /** Returns the underlying {@code field} for this {@code OptionDefinition}. */
  public Field getField() {
    return field;
  }

  /**
   * Returns the name of the option ("--name").
   *
   * <p>Labelled "Option" name to distinguish it from the field's name.
   */
  public String getOptionName() {
    return optionAnnotation.name();
  }

  /** The single-character abbreviation of the option ("-a"). */
  public char getAbbreviation() {
    return optionAnnotation.abbrev();
  }

  /** {@link Option#help()} */
  public String getHelpText() {
    return optionAnnotation.help();
  }

  /** {@link Option#valueHelp()} */
  public String getValueTypeHelpText() {
    return optionAnnotation.valueHelp();
  }

  /** {@link Option#defaultValue()} */
  public String getUnparsedDefaultValue() {
    return optionAnnotation.defaultValue();
  }

  /** {@link Option#category()} */
  public String getOptionCategory() {
    return optionAnnotation.category();
  }

  /** {@link Option#documentationCategory()} */
  public OptionDocumentationCategory getDocumentationCategory() {
    return optionAnnotation.documentationCategory();
  }

  /** {@link Option#effectTags()} */
  public OptionEffectTag[] getOptionEffectTags() {
    return optionAnnotation.effectTags();
  }

  /** {@link Option#metadataTags()} */
  public OptionMetadataTag[] getOptionMetadataTags() {
    return optionAnnotation.metadataTags();
  }

  /** {@link Option#converter()} ()} */
  @SuppressWarnings({"rawtypes"})
  public Class<? extends Converter> getProvidedConverter() {
    return optionAnnotation.converter();
  }

  /** {@link Option#allowMultiple()} */
  public boolean allowsMultiple() {
    return optionAnnotation.allowMultiple();
  }

  /** {@link Option#expansion()} */
  public String[] getOptionExpansion() {
    return optionAnnotation.expansion();
  }

  /** {@link Option#expansionFunction()} ()} */
  public Class<? extends ExpansionFunction> getExpansionFunction() {
    return optionAnnotation.expansionFunction();
  }

  /** {@link Option#implicitRequirements()} ()} */
  public String[] getImplicitRequirements() {
    return optionAnnotation.implicitRequirements();
  }

  /** {@link Option#deprecationWarning()} ()} */
  public String getDeprecationWarning() {
    return optionAnnotation.deprecationWarning();
  }

  /** {@link Option#oldName()} ()} ()} */
  public String getOldOptionName() {
    return optionAnnotation.oldName();
  }

  /** Returns whether an option --foo has a negative equivalent --nofoo. */
  public boolean hasNegativeOption() {
    return getType().equals(boolean.class) || getType().equals(TriState.class);
  }

  /** The type of the optionDefinition. */
  public Class<?> getType() {
    return field.getType();
  }

  /** Whether this field has type Void. */
  boolean isVoidField() {
    return getType().equals(Void.class);
  }

  public boolean isSpecialNullDefault() {
    return getUnparsedDefaultValue().equals("null") && !getType().isPrimitive();
  }

  /** Returns whether the arg is an expansion option. */
  public boolean isExpansionOption() {
    return (getOptionExpansion().length > 0 || usesExpansionFunction());
  }

  /** Returns whether the arg is an expansion option. */
  public boolean hasImplicitRequirements() {
    return (getImplicitRequirements().length > 0);
  }

  /**
   * Returns whether the arg is an expansion option defined by an expansion function (and not a
   * constant expansion value).
   */
  public boolean usesExpansionFunction() {
    return getExpansionFunction() != ExpansionFunction.class;
  }

  /**
   * For an option that does not use {@link Option#allowMultiple}, returns its type. For an option
   * that does use it, asserts that the type is a {@code List<T>} and returns its element type
   * {@code T}.
   */
  Type getFieldSingularType() {
    Type fieldType = getField().getGenericType();
    if (allowsMultiple()) {
      // The validity of the converter is checked at compile time. We know the type to be
      // List<singularType>.
      ParameterizedType pfieldType = (ParameterizedType) fieldType;
      fieldType = pfieldType.getActualTypeArguments()[0];
    }
    return fieldType;
  }

  /**
   * Retrieves the {@link Converter} that will be used for this option, taking into account the
   * default converters if an explicit one is not specified.
   *
   * <p>Memoizes the converter-finding logic to avoid repeating the computation.
   */
  public Converter<?> getConverter() {
    if (converter != null) {
      return converter;
    }
    Class<? extends Converter> converterClass = getProvidedConverter();
    if (converterClass == Converter.class) {
      // No converter provided, use the default one.
      Type type = getFieldSingularType();
      converter = Converters.DEFAULT_CONVERTERS.get(type);
    } else {
      try {
        // Instantiate the given Converter class.
        Constructor<?> constructor = converterClass.getConstructor();
        converter = (Converter<?>) constructor.newInstance();
      } catch (SecurityException | IllegalArgumentException | ReflectiveOperationException e) {
        // This indicates an error in the Converter, and should be discovered the first time it is
        // used.
        throw new ConstructionException(
            String.format("Error in the provided converter for option %s", getField().getName()),
            e);
      }
    }
    return converter;
  }

  /**
   * Returns whether a field should be considered as boolean.
   *
   * <p>Can be used for usage help and controlling whether the "no" prefix is allowed.
   */
  public boolean usesBooleanValueSyntax() {
    return getType().equals(boolean.class)
        || getType().equals(TriState.class)
        || getConverter() instanceof BoolOrEnumConverter;
  }

  /** Returns the evaluated default value for this option & memoizes the result. */
  public Object getDefaultValue() {
    if (defaultValue != null || isSpecialNullDefault()) {
      return defaultValue;
    }
    Converter<?> converter = getConverter();
    String defaultValueAsString = getUnparsedDefaultValue();
    boolean allowsMultiple = allowsMultiple();
    // If the option allows multiple values then we intentionally return the empty list as
    // the default value of this option since it is not always the case that an option
    // that allows multiple values will have a converter that returns a list value.
    if (allowsMultiple) {
      defaultValue = Collections.emptyList();
    } else {
      // Otherwise try to convert the default value using the converter
      try {
        defaultValue = converter.convert(defaultValueAsString);
      } catch (OptionsParsingException e) {
        throw new ConstructionException(
            String.format(
                "OptionsParsingException while retrieving the default value for %s: %s",
                getField().getName(), e.getMessage()),
            e);
      }
    }
    return defaultValue;
  }

  /**
   * {@link OptionDefinition} is really a wrapper around a {@link Field} that caches information
   * obtained through reflection. Checking that the fields they represent are equal is sufficient
   * to check that two {@link OptionDefinition} objects are equal.
   */
  @Override
  public boolean equals(Object object) {
    if (!(object instanceof OptionDefinition)) {
      return false;
    }
    OptionDefinition otherOption = (OptionDefinition) object;
    return field.equals(otherOption.field);
  }

  @Override
  public int hashCode() {
    return field.hashCode();
  }

  @Override
  public int compareTo(OptionDefinition o) {
    return getOptionName().compareTo(o.getOptionName());
  }

  @Override
  public String toString() {
    return String.format("option '--%s'", getOptionName());
  }

  static final Comparator<OptionDefinition> BY_OPTION_NAME =
      Comparator.comparing(OptionDefinition::getOptionName);

  /**
   * An ordering relation for option-field fields that first groups together options of the same
   * category, then sorts by name within the category.
   */
  static final Comparator<OptionDefinition> BY_CATEGORY =
      (left, right) -> {
        int r = left.getOptionCategory().compareTo(right.getOptionCategory());
        return r == 0 ? BY_OPTION_NAME.compare(left, right) : r;
      };
}