// 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.annotations.VisibleForTesting; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ListMultimap; import com.google.devtools.common.options.OptionsParser.ConstructionException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; /** * The value of an option. * *

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(); abstract void addOptionInstance( ParsedOptionDescription parsedOption, List warnings) throws OptionsParsingException; /** * For the given option, returns the correct type of OptionValueDescription, to which unparsed * values can be added. * *

The categories of option types are non-overlapping, an invariant checked by the * OptionProcessor at compile time. */ public static OptionValueDescription createOptionValueDescription(OptionDefinition option) { if (option.allowsMultiple()) { return new RepeatableOptionValueDescription(option); } else if (option.isExpansionOption()) { return new ExpansionOptionValueDescription(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); } 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 void addOptionInstance( ParsedOptionDescription parsedOption, List warnings) { throw new IllegalStateException( "Cannot add values to the default option value. Create a modifiable " + "OptionValueDescription using createOptionValueDescription() instead."); } } /** * The form of a value for a default type of flag, one that does not accumulate multiple values * and has no expansion. */ 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 void addOptionInstance( ParsedOptionDescription parsedOption, List warnings) throws OptionsParsingException { // This might be the first value, in that case, just store it! if (effectiveOptionInstance == null) { effectiveOptionInstance = parsedOption; effectiveValue = effectiveOptionInstance.getConvertedValue(); return; } // 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(); // Output warnings: if ((implicitDependent != null) && (optionThatDependsOnEffectiveValue != null)) { if (!implicitDependent.equals(optionThatDependsOnEffectiveValue)) { warnings.add( String.format( "Option '%s' is implicitly defined by both option '%s' and option '%s'", optionDefinition.getOptionName(), optionThatDependsOnEffectiveValue.getOptionName(), implicitDependent.getOptionName())); } } else if ((implicitDependent != null) && parsedOption.getPriority().equals(effectiveOptionInstance.getPriority())) { warnings.add( String.format( "Option '%s' is implicitly defined by option '%s'; the implicitly set value " + "overrides the previous one", optionDefinition.getOptionName(), implicitDependent.getOptionName())); } else if (optionThatDependsOnEffectiveValue != null) { warnings.add( String.format( "A new value for option '%s' overrides a previous implicit setting of that " + "option by option '%s'", optionDefinition.getOptionName(), optionThatDependsOnEffectiveValue.getOptionName())); } else if ((parsedOption.getPriority() == effectiveOptionInstance.getPriority()) && ((optionThatExpandedToEffectiveValue == null) && (expandedFrom != null))) { // Create a warning if an expansion option overrides an explicit option: warnings.add( String.format( "The option '%s' was expanded and now overrides a previous explicitly specified " + "option '%s'", expandedFrom.getOptionName(), optionDefinition.getOptionName())); } else if ((optionThatExpandedToEffectiveValue != null) && (expandedFrom != null)) { warnings.add( String.format( "The option '%s' was expanded to from both options '%s' and '%s'", optionDefinition.getOptionName(), optionThatExpandedToEffectiveValue.getOptionName(), expandedFrom.getOptionName())); } // Record the new value: effectiveOptionInstance = parsedOption; effectiveValue = parsedOption.getConvertedValue(); } else { // The new value does not override the old value, as it has lower priority. warnings.add( String.format( "The lower priority option '%s' (source %s) does not override the previous value " + "'%s'", parsedOption.getCommandLineForm(), parsedOption.getSource(), effectiveOptionInstance.getCommandLineForm())); } } @VisibleForTesting ParsedOptionDescription getEffectiveOptionInstance() { return effectiveOptionInstance; } } /** The form of a value for an option that accumulates multiple values on the command line. */ static class RepeatableOptionValueDescription extends OptionValueDescription { ListMultimap parsedOptions; ListMultimap 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() .values() .stream() .flatMap(Collection::stream) .map(ParsedOptionDescription::getSource) .distinct() .collect(Collectors.joining(", ")); } @Override public List 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. It was already checked in // the constructor, so this is type-safe. List result = new ArrayList<>(); for (OptionPriority priority : OptionPriority.values()) { // If there is no mapping for this key, this check avoids object creation (because // ListMultimap has to return a new object on get) and also an unnecessary addAll call. if (optionValues.containsKey(priority)) { result.addAll(optionValues.get(priority)); } } return result; } @Override void addOptionInstance( ParsedOptionDescription parsedOption, List 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); } } } /** * 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()}. */ static class ExpansionOptionValueDescription extends OptionValueDescription { private ExpansionOptionValueDescription(OptionDefinition optionDefinition) { super(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 void addOptionInstance( ParsedOptionDescription parsedOption, List warnings) { // TODO(b/65540004) Deal with expansion options here instead of in parse(), and track their // link to the options they expanded to to. } } /** The form of a value for a flag with implicit requirements. */ 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 void addOptionInstance( ParsedOptionDescription parsedOption, List warnings) throws OptionsParsingException { // This is a valued flag, its value is handled the same way as a normal // SingleOptionValueDescription. super.addOptionInstance(parsedOption, warnings); // Now deal with the implicit requirements. // TODO(b/65540004) Deal with options with implicit requirements here instead of in parse(), // and track their link to the options they implicitly expanded to to. } } /** Form for options that contain other options in the value text to which they expand. */ static final class WrapperOptionValueDescription extends OptionValueDescription { WrapperOptionValueDescription(OptionDefinition optionDefinition) { super(optionDefinition); } @Override public Object getValue() { return null; } @Override public String getSourceString() { return null; } @Override void addOptionInstance( ParsedOptionDescription parsedOption, List warnings) throws OptionsParsingException { // TODO(b/65540004) Deal with options with implicit requirements here instead of in parse(), // and track their link to the options they implicitly expanded to to. } } }