blob: 71d05de7273a0f0598eb8e01b7cc6f8cbb1a1030 [file]
// Copyright 2024 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 static java.util.Comparator.comparing;
import com.google.common.collect.ImmutableList;
import java.lang.reflect.Constructor;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import javax.annotation.Nullable;
/** Everything the {@link OptionsParser} needs to know about how an option is defined. */
public abstract class OptionDefinition implements Comparable<OptionDefinition> {
/**
* A special value used to specify an absence of default value.
*
* @see Option#defaultValue
*/
public static final String SPECIAL_NULL_DEFAULT_VALUE = "null";
/** An ordering relation for options that orders by the option name. */
public static final Comparator<OptionDefinition> BY_OPTION_NAME =
Comparator.comparing(OptionDefinition::getOptionName);
/**
* An ordering relation for options that first groups together options of the same category, then
* sorts by name within the category.
*/
public static final Comparator<OptionDefinition> BY_CATEGORY =
comparing(OptionDefinition::getOptionCategory).thenComparing(BY_OPTION_NAME);
/** Returns all options fields of the given options class, in alphabetic order. */
public static ImmutableList<? extends OptionDefinition> getOptionDefinitions(
Class<? extends OptionsBase> optionsClass) {
return OptionsData.getAllOptionDefinitionsForClass(optionsClass);
}
/**
* Two option definitions are considered equivalent for parsing if they result in the same control
* flow through {@link OptionsParserImpl#identifyOptionAndPossibleArgument}. This is crucial to
* ensure that the beginning of the next option can be determined unambiguously when parsing with
* fallback data.
*
* <p>Examples:
*
* <ul>
* <li>Both {@code query} and {@code cquery} have a {@code --output} option, but the options
* accept different sets of values (e.g. {@code cquery} has {@code --output=files}, but
* {@code query} doesn't. However, since both options accept a string value, they parse
* equivalently as far as {@link OptionsParserImpl#identifyOptionAndPossibleArgument} is
* concerned - potential failures due to unsupported values occur after parsing, during
* value conversion. There is no ambiguity in how many command-line arguments are consumed
* depending on which option definition is used.
* <li>If the hypothetical {@code foo} command also had a {@code --output} option, but it were
* boolean-valued, then the two option definitions would <b>not</b> be equivalent for
* parsing: The command line {@code --output --copt=foo} would parse as {@code {"output":
* "--copt=foo"}} for the {@code cquery} command, but as {@code {"output": true, "copt":
* "foo"}} for the {@code foo} command, thus resulting in parsing ambiguities between the
* two commands.
* </ul>
*/
public static boolean equivalentForParsing(
OptionDefinition definition, OptionDefinition otherDefinition) {
if (definition.equals(otherDefinition)) {
return true;
}
return (definition.usesBooleanValueSyntax() == otherDefinition.usesBooleanValueSyntax())
&& (definition.getType().equals(Void.class) == otherDefinition.getType().equals(Void.class))
&& (ImmutableList.copyOf(definition.getOptionMetadataTags())
.contains(OptionMetadataTag.INTERNAL)
== ImmutableList.copyOf(otherDefinition.getOptionMetadataTags())
.contains(OptionMetadataTag.INTERNAL));
}
protected final Option optionAnnotation;
private volatile Converter<?> converter = null;
private volatile Object defaultValue = null;
protected OptionDefinition(Option optionAnnotation) {
this.optionAnnotation = optionAnnotation;
}
/** Returns the declaring {@link OptionsBase} class that owns this option. */
public abstract <C extends OptionsBase> Class<? extends C> getDeclaringClass(Class<C> baseClass);
/**
* Returns the raw value of the option. Use {@link #getValue} if possible to correctly handle
* default values.
*/
public abstract Object getRawValue(OptionsBase optionsBase);
/** Returns the value of this option, taking default values into account. */
public Object getValue(OptionsBase optionsBase) {
Object value = getRawValue(optionsBase);
if (value == null && !isSpecialNullDefault()) {
value = getUnparsedDefaultValue();
}
return value;
}
/**
* Returns the value of this option as a boolean. If the option is not boolean-typed, throws an
* IllegalStateException.
*/
public boolean getBooleanValue(OptionsBase optionsBase) {
// Check for primitive boolean first, as it's more common.
if (!getType().isAssignableFrom(Boolean.TYPE) && !getType().isAssignableFrom(Boolean.class)) {
throw new IllegalStateException(
"Option "
+ getOptionName()
+ " is not a boolean, has type "
+ getType().getCanonicalName());
}
return getValue(optionsBase).equals(Boolean.TRUE);
}
/** Sets the value for this option. */
public abstract void setValue(OptionsBase optionsBase, Object value);
/** Returns whether this option is deprecated. */
public abstract boolean isDeprecated();
/** Returns the name of this option. */
public String getOptionName() {
return optionAnnotation.name();
}
/** Returns a one-character abbreviation for this option, if any. */
public char getAbbreviation() {
return optionAnnotation.abbrev();
}
/** Returns the help test for this option. */
public String getHelpText() {
return optionAnnotation.help();
}
/** Returns a short description of the expected type of this option. */
public String getValueTypeHelpText() {
return optionAnnotation.valueHelp();
}
/**
* Returns the default value of this option, with no conversion performed. Should only be used by
* the parser.
*/
public String getUnparsedDefaultValue() {
return optionAnnotation.defaultValue();
}
/**
* Returns the deprecated option category.
*
* @deprecated Use {@link #getDocumentationCategory} instead
*/
@Deprecated
public String getOptionCategory() {
return optionAnnotation.category();
}
/** Returns the option category. */
public OptionDocumentationCategory getDocumentationCategory() {
return optionAnnotation.documentationCategory();
}
/** Returns data about the intended effects of this option. */
public OptionEffectTag[] getOptionEffectTags() {
return optionAnnotation.effectTags();
}
/** Returns metadata about this option. */
public OptionMetadataTag[] getOptionMetadataTags() {
return optionAnnotation.metadataTags();
}
/** Returns a converter to use for this option. */
@SuppressWarnings({"rawtypes"})
public Class<? extends Converter> getProvidedConverter() {
return optionAnnotation.converter();
}
/** Returns whether this option allows multiple instances to be combined into a list. */
public boolean allowsMultiple() {
return optionAnnotation.allowMultiple();
}
/** Returns any options which are added if this option is present. */
public String[] getOptionExpansion() {
return optionAnnotation.expansion();
}
/** Returns additional options that need to be implicitly added for this option. */
public String[] getImplicitRequirements() {
return optionAnnotation.implicitRequirements();
}
/** Returns a deprecation warning for this option, if one is present. */
public String getDeprecationWarning() {
return optionAnnotation.deprecationWarning();
}
/** Returns the old name for this option, if one is present. */
public String getOldOptionName() {
return optionAnnotation.oldName();
}
/** Returns a warning to use with this option if the old name is specified. */
public boolean getOldNameWarning() {
return optionAnnotation.oldNameWarning();
}
/** The type of the optionDefinition. */
public abstract Class<?> getType();
/** Whether this field has type Void. */
public boolean isVoidField() {
return getType().equals(Void.class);
}
// TODO: blaze-configurability - try to remove special handling for defaults
public boolean isSpecialNullDefault() {
return getUnparsedDefaultValue().equals(SPECIAL_NULL_DEFAULT_VALUE) && !getType().isPrimitive();
}
/** Returns whether the arg is an expansion option. */
public boolean isExpansionOption() {
return getOptionExpansion().length > 0;
}
/** Returns whether the arg is an expansion option. */
public boolean hasImplicitRequirements() {
return (getImplicitRequirements().length > 0);
}
/**
* 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}.
*/
public Type getFieldSingularType() {
Type type = getSingularType();
if (allowsMultiple()) {
// The validity of the converter is checked at compile time. We know the type to be
// List<singularType>.
ParameterizedType pfieldType = (ParameterizedType) type;
type = pfieldType.getActualTypeArguments()[0];
}
return type;
}
protected abstract Type getSingularType();
/** Returns the {@link Converter} that will be used for this option. */
public Converter<?> getConverter() {
if (converter != null) {
return converter;
}
synchronized (this) {
if (converter != null) {
return converter;
}
@SuppressWarnings("rawtypes") // Converter itself has a type argument
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.getDeclaredConstructor();
constructor.setAccessible(true);
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", getMemberName()), 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
|| getConverter() instanceof BooleanStyleOption;
}
/**
* Returns whether an option requires a value when instantiated, or instead can be present without
* an explicit value.
*/
public boolean requiresValue() {
return !isVoidField() && !usesBooleanValueSyntax();
}
/**
* Wraps a converted default value into a {@link List} if the converter doesn't do it on its own.
*
* <p>This is to make sure multiple ({@link Option#allowMultiple()}) options' default values are
* always converted to a list representation.
*/
@SuppressWarnings("unchecked") // Not an unchecked cast - there's an explicit type check before it
protected static List<Object> maybeWrapMultipleDefaultValue(Object convertedDefaultValue) {
if (convertedDefaultValue instanceof List) {
return (List<Object>) convertedDefaultValue;
} else {
return Arrays.asList(convertedDefaultValue);
}
}
/** Returns the evaluated default value for this option. */
@Nullable
public Object getDefaultValue(@Nullable Object conversionContext) {
if (defaultValue != null) {
return defaultValue;
}
synchronized (this) {
if (defaultValue != null) {
return defaultValue;
}
if (isSpecialNullDefault()) {
return allowsMultiple() ? ImmutableList.of() : null;
}
Converter<?> converter = getConverter();
String defaultValueAsString = getUnparsedDefaultValue();
try {
Object convertedDefaultValue = converter.convert(defaultValueAsString, conversionContext);
defaultValue =
allowsMultiple()
? maybeWrapMultipleDefaultValue(convertedDefaultValue)
: convertedDefaultValue;
} catch (OptionsParsingException e) {
throw new ConstructionException(
String.format(
"OptionsParsingException while retrieving the default value for %s: %s",
getMemberName(), e.getMessage()),
e);
}
return defaultValue;
}
}
/** Returns the name of the member (field or method) that defines this option. */
public abstract String getMemberName();
@Override
public int compareTo(OptionDefinition o) {
return getOptionName().compareTo(o.getOptionName());
}
}