| // 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 whether this option has the FULLY_REDACTED_IN_LOGS metadata tag. */ | |
| public boolean isFullyRedactedInLogs() { | |
| for (OptionMetadataTag tag : getOptionMetadataTags()) { | |
| if (tag == OptionMetadataTag.FULLY_REDACTED_IN_LOGS) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| /** 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()); | |
| } | |
| } |