// 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()} ()} */ 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(); } /** {@link Option#wrapperOption()} ()} ()} */ public boolean isWrapperOption() { return optionAnnotation.wrapperOption(); } /** 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; }; }