Home | History | Annotate | Download | only in options
      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 {
     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    */
     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 
     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()} ()} */
    140   Class<? extends ExpansionFunction> getExpansionFunction() {
    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 
    159   /** {@link Option#wrapperOption()} ()} ()} */
    160   public boolean isWrapperOption() {
    161     return optionAnnotation.wrapperOption();
    162   }
    163 
    164   /** Returns whether an option --foo has a negative equivalent --nofoo. */
    165   public boolean hasNegativeOption() {
    166     return getType().equals(boolean.class) || getType().equals(TriState.class);
    167   }
    168 
    169   /** The type of the optionDefinition. */
    170   public Class<?> getType() {
    171     return field.getType();
    172   }
    173 
    174   /** Whether this field has type Void. */
    175   boolean isVoidField() {
    176     return getType().equals(Void.class);
    177   }
    178 
    179   public boolean isSpecialNullDefault() {
    180     return getUnparsedDefaultValue().equals("null") && !getType().isPrimitive();
    181   }
    182 
    183   /** Returns whether the arg is an expansion option. */
    184   public boolean isExpansionOption() {
    185     return (getOptionExpansion().length > 0 || usesExpansionFunction());
    186   }
    187 
    188   /** Returns whether the arg is an expansion option. */
    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    */
    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    */
    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    */
    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    */
    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. */
    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
    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
    302   public int hashCode() {
    303     return field.hashCode();
    304   }
    305 
    306   @Override
    307   public int compareTo(OptionDefinition o) {
    308     return getOptionName().compareTo(o.getOptionName());
    309   }
    310 
    311   @Override
    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