| // 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.common.base.Preconditions; |
| import com.google.common.collect.ArrayListMultimap; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ListMultimap; |
| import com.google.devtools.common.options.OptionPriority.PriorityCategory; |
| import com.google.devtools.common.options.OptionsParser.ConstructionException; |
| import java.util.Collection; |
| import java.util.Comparator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.stream.Collectors; |
| import javax.annotation.Nullable; |
| |
| /** |
| * The value of an option. |
| * |
| * <p>This takes care of tracking the final value as multiple instances of an option are parsed. |
| */ |
| public abstract class OptionValueDescription { |
| |
| protected final OptionDefinition optionDefinition; |
| |
| public OptionValueDescription(OptionDefinition optionDefinition) { |
| this.optionDefinition = optionDefinition; |
| } |
| |
| public OptionDefinition getOptionDefinition() { |
| return optionDefinition; |
| } |
| |
| /** Returns the current or final value of this option. */ |
| public abstract Object getValue(); |
| |
| /** Returns the source(s) of this option, if there were multiple, duplicates are removed. */ |
| public abstract String getSourceString(); |
| |
| /** |
| * Add an instance of the option to this value. The various types of options are in charge of |
| * making sure that the value is correctly stored, with proper tracking of its priority and |
| * placement amongst other options. |
| * |
| * @return a bundle containing arguments that need to be parsed further. |
| */ |
| abstract ExpansionBundle addOptionInstance( |
| ParsedOptionDescription parsedOption, List<String> warnings) throws OptionsParsingException; |
| |
| /** |
| * Grouping of convenience for the options that expand to other options, to attach an |
| * option-appropriate source string along with the options that need to be parsed. |
| */ |
| public static class ExpansionBundle { |
| List<String> expansionArgs; |
| String sourceOfExpansionArgs; |
| |
| public ExpansionBundle(List<String> args, String source) { |
| expansionArgs = args; |
| sourceOfExpansionArgs = source; |
| } |
| } |
| |
| /** |
| * Returns the canonical instances of this option - the instances that affect the current value. |
| * |
| * <p>For options that do not have values in their own right, this should be the empty list. In |
| * contrast, the DefaultOptionValue does not have a canonical form at all, since it was never set, |
| * and is null. |
| */ |
| @Nullable |
| public abstract List<ParsedOptionDescription> getCanonicalInstances(); |
| |
| /** |
| * For the given option, returns the correct type of OptionValueDescription, to which unparsed |
| * values can be added. |
| * |
| * <p>The categories of option types are non-overlapping, an invariant checked by the |
| * OptionProcessor at compile time. |
| */ |
| public static OptionValueDescription createOptionValueDescription( |
| OptionDefinition option, OptionsData optionsData) { |
| if (option.isExpansionOption()) { |
| return new ExpansionOptionValueDescription(option, optionsData); |
| } else if (option.allowsMultiple()) { |
| return new RepeatableOptionValueDescription(option); |
| } else if (option.hasImplicitRequirements()) { |
| return new OptionWithImplicitRequirementsValueDescription(option); |
| } else { |
| return new SingleOptionValueDescription(option); |
| } |
| } |
| |
| /** |
| * For options that have not been set, this will return a correct OptionValueDescription for the |
| * default value. |
| */ |
| public static OptionValueDescription getDefaultOptionValue(OptionDefinition option) { |
| return new DefaultOptionValueDescription(option); |
| } |
| |
| private static class DefaultOptionValueDescription extends OptionValueDescription { |
| |
| private DefaultOptionValueDescription(OptionDefinition optionDefinition) { |
| super(optionDefinition); |
| } |
| |
| @Override |
| public Object getValue() { |
| return optionDefinition.getDefaultValue(); |
| } |
| |
| @Override |
| public String getSourceString() { |
| return null; |
| } |
| |
| @Override |
| ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings) { |
| throw new IllegalStateException( |
| "Cannot add values to the default option value. Create a modifiable " |
| + "OptionValueDescription using createOptionValueDescription() instead."); |
| } |
| |
| @Override |
| public ImmutableList<ParsedOptionDescription> getCanonicalInstances() { |
| return null; |
| } |
| } |
| |
| /** |
| * The form of a value for a default type of flag, one that does not accumulate multiple values |
| * and has no expansion. |
| */ |
| private static class SingleOptionValueDescription extends OptionValueDescription { |
| private ParsedOptionDescription effectiveOptionInstance; |
| private Object effectiveValue; |
| |
| private SingleOptionValueDescription(OptionDefinition optionDefinition) { |
| super(optionDefinition); |
| if (optionDefinition.allowsMultiple()) { |
| throw new ConstructionException("Can't have a single value for an allowMultiple option."); |
| } |
| if (optionDefinition.isExpansionOption()) { |
| throw new ConstructionException("Can't have a single value for an expansion option."); |
| } |
| effectiveOptionInstance = null; |
| effectiveValue = null; |
| } |
| |
| @Override |
| public Object getValue() { |
| return effectiveValue; |
| } |
| |
| @Override |
| public String getSourceString() { |
| return effectiveOptionInstance.getSource(); |
| } |
| |
| // Warnings should not end with a '.' because the internal reporter adds one automatically. |
| @Override |
| ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings) |
| throws OptionsParsingException { |
| // This might be the first value, in that case, just store it! |
| if (effectiveOptionInstance == null) { |
| effectiveOptionInstance = parsedOption; |
| effectiveValue = effectiveOptionInstance.getConvertedValue(); |
| return null; |
| } |
| |
| // If there was another value, check whether the new one will override it, and if so, |
| // log warnings describing the change. |
| if (parsedOption.getPriority().compareTo(effectiveOptionInstance.getPriority()) >= 0) { |
| // Identify the option that might have led to the current and new value of this option. |
| ParsedOptionDescription implicitDependent = parsedOption.getImplicitDependent(); |
| ParsedOptionDescription expandedFrom = parsedOption.getExpandedFrom(); |
| ParsedOptionDescription optionThatDependsOnEffectiveValue = |
| effectiveOptionInstance.getImplicitDependent(); |
| ParsedOptionDescription optionThatExpandedToEffectiveValue = |
| effectiveOptionInstance.getExpandedFrom(); |
| |
| Object newValue = parsedOption.getConvertedValue(); |
| // Output warnings if there is conflicting options set different values in a way that might |
| // not have been obvious to the user, such as through expansions and implicit requirements. |
| if (effectiveValue != null && !effectiveValue.equals(newValue)) { |
| boolean samePriorityCategory = |
| parsedOption |
| .getPriority() |
| .getPriorityCategory() |
| .equals(effectiveOptionInstance.getPriority().getPriorityCategory()); |
| if ((implicitDependent != null) && (optionThatDependsOnEffectiveValue != null)) { |
| if (!implicitDependent.equals(optionThatDependsOnEffectiveValue)) { |
| warnings.add( |
| String.format( |
| "%s is implicitly defined by both %s and %s", |
| optionDefinition, optionThatDependsOnEffectiveValue, implicitDependent)); |
| } |
| } else if ((implicitDependent != null) && samePriorityCategory) { |
| warnings.add( |
| String.format( |
| "%s is implicitly defined by %s; the implicitly set value " |
| + "overrides the previous one", |
| optionDefinition, implicitDependent)); |
| } else if (optionThatDependsOnEffectiveValue != null) { |
| warnings.add( |
| String.format( |
| "A new value for %s overrides a previous implicit setting of that " |
| + "option by %s", |
| optionDefinition, optionThatDependsOnEffectiveValue)); |
| } else if (samePriorityCategory |
| && parsedOption |
| .getPriority() |
| .getPriorityCategory() |
| .equals(PriorityCategory.COMMAND_LINE) |
| && ((optionThatExpandedToEffectiveValue == null) && (expandedFrom != null))) { |
| // Create a warning if an expansion option overrides an explicit option: |
| warnings.add( |
| String.format( |
| "%s was expanded and now overrides the explicit option %s with %s", |
| expandedFrom, |
| effectiveOptionInstance.getCommandLineForm(), |
| parsedOption.getCommandLineForm())); |
| } else if ((optionThatExpandedToEffectiveValue != null) && (expandedFrom != null)) { |
| warnings.add( |
| String.format( |
| "%s was expanded to from both %s and %s", |
| optionDefinition, optionThatExpandedToEffectiveValue, expandedFrom)); |
| } |
| } |
| |
| // Record the new value: |
| effectiveOptionInstance = parsedOption; |
| effectiveValue = newValue; |
| } |
| return null; |
| } |
| |
| @Override |
| public ImmutableList<ParsedOptionDescription> getCanonicalInstances() { |
| // If the current option is an implicit requirement, we don't need to list this value since |
| // the parent implies it. In this case, it is sufficient to not list this value at all. |
| if (effectiveOptionInstance.getImplicitDependent() == null) { |
| return ImmutableList.of(effectiveOptionInstance); |
| } |
| return ImmutableList.of(); |
| } |
| } |
| |
| /** The form of a value for an option that accumulates multiple values on the command line. */ |
| private static class RepeatableOptionValueDescription extends OptionValueDescription { |
| ListMultimap<OptionPriority, ParsedOptionDescription> parsedOptions; |
| ListMultimap<OptionPriority, Object> optionValues; |
| |
| private RepeatableOptionValueDescription(OptionDefinition optionDefinition) { |
| super(optionDefinition); |
| if (!optionDefinition.allowsMultiple()) { |
| throw new ConstructionException( |
| "Can't have a repeated value for a non-allowMultiple option."); |
| } |
| parsedOptions = ArrayListMultimap.create(); |
| optionValues = ArrayListMultimap.create(); |
| } |
| |
| @Override |
| public String getSourceString() { |
| return parsedOptions |
| .asMap() |
| .entrySet() |
| .stream() |
| .sorted(Comparator.comparing(Map.Entry::getKey)) |
| .map(Map.Entry::getValue) |
| .flatMap(Collection::stream) |
| .map(ParsedOptionDescription::getSource) |
| .distinct() |
| .collect(Collectors.joining(", ")); |
| } |
| |
| @Override |
| public ImmutableList<Object> getValue() { |
| // Sort the results by option priority and return them in a new list. The generic type of |
| // the list is not known at runtime, so we can't use it here. |
| return optionValues.asMap().entrySet().stream() |
| .sorted(Comparator.comparing(Map.Entry::getKey)) |
| .map(Map.Entry::getValue) |
| .flatMap(Collection::stream) |
| .collect(ImmutableList.toImmutableList()); |
| } |
| |
| @Override |
| ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings) |
| throws OptionsParsingException { |
| // For repeatable options, we allow flags that take both single values and multiple values, |
| // potentially collapsing them down. |
| Object convertedValue = parsedOption.getConvertedValue(); |
| OptionPriority priority = parsedOption.getPriority(); |
| parsedOptions.put(priority, parsedOption); |
| if (convertedValue instanceof List<?>) { |
| optionValues.putAll(priority, (List<?>) convertedValue); |
| } else { |
| optionValues.put(priority, convertedValue); |
| } |
| return null; |
| } |
| |
| @Override |
| public ImmutableList<ParsedOptionDescription> getCanonicalInstances() { |
| return parsedOptions |
| .asMap() |
| .entrySet() |
| .stream() |
| .sorted(Comparator.comparing(Map.Entry::getKey)) |
| .map(Map.Entry::getValue) |
| .flatMap(Collection::stream) |
| // Only provide the options that aren't implied elsewhere. |
| .filter(optionDesc -> optionDesc.getImplicitDependent() == null) |
| .collect(ImmutableList.toImmutableList()); |
| } |
| } |
| |
| /** |
| * The form of a value for an expansion option, one that does not have its own value but expands |
| * in place to other options. This should be used for both flags with a static expansion defined |
| * in {@link Option#expansion()} and flags with an {@link Option#expansionFunction()}. |
| */ |
| private static class ExpansionOptionValueDescription extends OptionValueDescription { |
| private final List<String> expansion; |
| |
| private ExpansionOptionValueDescription( |
| OptionDefinition optionDefinition, OptionsData optionsData) { |
| super(optionDefinition); |
| this.expansion = optionsData.getEvaluatedExpansion(optionDefinition); |
| if (!optionDefinition.isExpansionOption()) { |
| throw new ConstructionException( |
| "Options without expansions can't be tracked using ExpansionOptionValueDescription"); |
| } |
| } |
| |
| @Override |
| public Object getValue() { |
| return null; |
| } |
| |
| @Override |
| public String getSourceString() { |
| return null; |
| } |
| |
| @Override |
| ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings) { |
| if (parsedOption.getUnconvertedValue() != null |
| && !parsedOption.getUnconvertedValue().isEmpty()) { |
| warnings.add( |
| String.format( |
| "%s is an expansion option. It does not accept values, and does not change its " |
| + "expansion based on the value provided. Value '%s' will be ignored.", |
| optionDefinition, parsedOption.getUnconvertedValue())); |
| } |
| |
| return new ExpansionBundle( |
| expansion, |
| (parsedOption.getSource() == null) |
| ? String.format("expanded from %s", optionDefinition) |
| : String.format( |
| "expanded from %s (source %s)", optionDefinition, parsedOption.getSource())); |
| } |
| |
| @Override |
| public ImmutableList<ParsedOptionDescription> getCanonicalInstances() { |
| // The options this expands to are incorporated in their own right - this option does |
| // not have a canonical form. |
| return ImmutableList.of(); |
| } |
| } |
| |
| /** The form of a value for a flag with implicit requirements. */ |
| private static class OptionWithImplicitRequirementsValueDescription |
| extends SingleOptionValueDescription { |
| |
| private OptionWithImplicitRequirementsValueDescription(OptionDefinition optionDefinition) { |
| super(optionDefinition); |
| if (!optionDefinition.hasImplicitRequirements()) { |
| throw new ConstructionException( |
| "Options without implicit requirements can't be tracked using " |
| + "OptionWithImplicitRequirementsValueDescription"); |
| } |
| } |
| |
| @Override |
| ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings) |
| throws OptionsParsingException { |
| // This is a valued flag, its value is handled the same way as a normal |
| // SingleOptionValueDescription. (We check at compile time that these flags aren't |
| // "allowMultiple") |
| ExpansionBundle superExpansion = super.addOptionInstance(parsedOption, warnings); |
| Preconditions.checkArgument( |
| superExpansion == null, "SingleOptionValueDescription should not expand to anything."); |
| if (parsedOption.getConvertedValue().equals(optionDefinition.getDefaultValue())) { |
| warnings.add( |
| String.format( |
| "%s sets %s to its default value. Since this option has implicit requirements that " |
| + "are set whenever the option is explicitly provided, regardless of the " |
| + "value, this will behave differently than letting a default be a default. " |
| + "Specifically, this options expands to {%s}.", |
| parsedOption.getCommandLineForm(), |
| optionDefinition, |
| String.join(" ", optionDefinition.getImplicitRequirements()))); |
| } |
| |
| // Now deal with the implicit requirements. |
| return new ExpansionBundle( |
| ImmutableList.copyOf(optionDefinition.getImplicitRequirements()), |
| (parsedOption.getSource() == null) |
| ? String.format("implicit requirement of %s", optionDefinition) |
| : String.format( |
| "implicit requirement of %s (source %s)", |
| optionDefinition, parsedOption.getSource())); |
| } |
| } |
| } |
| |
| |