// 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.OptionsParser.ConstructionException; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Map.Entry; 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 if (option.isWrapperOption()) { return new WrapperOptionValueDescription(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. OptionDefinition implicitDependent = parsedOption.getImplicitDependent(); OptionDefinition expandedFrom = parsedOption.getExpandedFrom(); OptionDefinition optionThatDependsOnEffectiveValue = effectiveOptionInstance.getImplicitDependent(); OptionDefinition 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.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 && ((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 a previous explicitly specified %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(Entry::getKey)) .map(Entry::getValue) .flatMap(Collection::stream) .map(ParsedOptionDescription::getSource) .distinct() .collect(Collectors.joining(", ")); } @Override public List<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(Entry::getKey)) .map(Entry::getValue) .flatMap(Collection::stream) .collect(Collectors.toList()); } @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(Entry::getKey)) .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())); } } /** Form for options that contain other options in the value text to which they expand. */ private static final class WrapperOptionValueDescription extends OptionValueDescription { WrapperOptionValueDescription(OptionDefinition optionDefinition) { super(optionDefinition); } @Override public Object getValue() { return null; } @Override public String getSourceString() { return null; } @Override ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings) throws OptionsParsingException { if (!parsedOption.getUnconvertedValue().startsWith("-")) { throw new OptionsParsingException( String.format( "Invalid value format for %s. You may have meant --%s=--%s", optionDefinition, optionDefinition.getOptionName(), parsedOption.getUnconvertedValue())); } return new ExpansionBundle( ImmutableList.of(parsedOption.getUnconvertedValue()), (parsedOption.getSource() == null) ? String.format("unwrapped from %s", optionDefinition) : String.format( "unwrapped from %s (source %s)", optionDefinition, parsedOption.getSource())); } @Override public ImmutableList<ParsedOptionDescription> getCanonicalInstances() { // No wrapper options get listed in the canonical form - the options they are wrapping will // be in the right place. return ImmutableList.of(); } } }