// 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.
*
*
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 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 expansionArgs;
String sourceOfExpansionArgs;
public ExpansionBundle(List args, String source) {
expansionArgs = args;
sourceOfExpansionArgs = source;
}
}
/**
* Returns the canonical instances of this option - the instances that affect the current value.
*
*
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 getCanonicalInstances();
/**
* 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, 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 warnings) {
throw new IllegalStateException(
"Cannot add values to the default option value. Create a modifiable "
+ "OptionValueDescription using createOptionValueDescription() instead.");
}
@Override
public ImmutableList 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 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 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 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()
.entrySet()
.stream()
.sorted(Comparator.comparing(Entry::getKey))
.map(Entry::getValue)
.flatMap(Collection::stream)
.map(ParsedOptionDescription::getSource)
.distinct()
.collect(Collectors.joining(", "));
}
@Override
public List