Home | History | Annotate | Download | only in options
      1 // Copyright 2017 The Bazel Authors. All rights reserved.
      2 //
      3 // Licensed under the Apache License, Version 2.0 (the "License");
      4 // you may not use this file except in compliance with the License.
      5 // You may obtain a copy of the License at
      6 //
      7 //    http://www.apache.org/licenses/LICENSE-2.0
      8 //
      9 // Unless required by applicable law or agreed to in writing, software
     10 // distributed under the License is distributed on an "AS IS" BASIS,
     11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 // See the License for the specific language governing permissions and
     13 // limitations under the License.
     14 
     15 package com.google.devtools.common.options;
     16 
     17 import com.google.common.base.Preconditions;
     18 import com.google.common.collect.ArrayListMultimap;
     19 import com.google.common.collect.ImmutableList;
     20 import com.google.common.collect.ListMultimap;
     21 import com.google.devtools.common.options.OptionsParser.ConstructionException;
     22 import java.util.Collection;
     23 import java.util.Comparator;
     24 import java.util.List;
     25 import java.util.Map;
     26 import java.util.stream.Collectors;
     27 import javax.annotation.Nullable;
     28 
     29 /**
     30  * The value of an option.
     31  *
     32  * <p>This takes care of tracking the final value as multiple instances of an option are parsed.
     33  */
     34 public abstract class OptionValueDescription {
     35 
     36   protected final OptionDefinition optionDefinition;
     37 
     38   public OptionValueDescription(OptionDefinition optionDefinition) {
     39     this.optionDefinition = optionDefinition;
     40   }
     41 
     42   public OptionDefinition getOptionDefinition() {
     43     return optionDefinition;
     44   }
     45 
     46   /** Returns the current or final value of this option. */
     47   public abstract Object getValue();
     48 
     49   /** Returns the source(s) of this option, if there were multiple, duplicates are removed. */
     50   public abstract String getSourceString();
     51 
     52   /**
     53    * Add an instance of the option to this value. The various types of options are in charge of
     54    * making sure that the value is correctly stored, with proper tracking of its priority and
     55    * placement amongst other options.
     56    *
     57    * @return a bundle containing arguments that need to be parsed further.
     58    */
     59   abstract ExpansionBundle addOptionInstance(
     60       ParsedOptionDescription parsedOption, List<String> warnings) throws OptionsParsingException;
     61 
     62   /**
     63    * Grouping of convenience for the options that expand to other options, to attach an
     64    * option-appropriate source string along with the options that need to be parsed.
     65    */
     66   public static class ExpansionBundle {
     67     List<String> expansionArgs;
     68     String sourceOfExpansionArgs;
     69 
     70     public ExpansionBundle(List<String> args, String source) {
     71       expansionArgs = args;
     72       sourceOfExpansionArgs = source;
     73     }
     74   }
     75 
     76   /**
     77    * Returns the canonical instances of this option - the instances that affect the current value.
     78    *
     79    * <p>For options that do not have values in their own right, this should be the empty list. In
     80    * contrast, the DefaultOptionValue does not have a canonical form at all, since it was never set,
     81    * and is null.
     82    */
     83   @Nullable
     84   public abstract List<ParsedOptionDescription> getCanonicalInstances();
     85 
     86   /**
     87    * For the given option, returns the correct type of OptionValueDescription, to which unparsed
     88    * values can be added.
     89    *
     90    * <p>The categories of option types are non-overlapping, an invariant checked by the
     91    * OptionProcessor at compile time.
     92    */
     93   public static OptionValueDescription createOptionValueDescription(
     94       OptionDefinition option, OptionsData optionsData) {
     95     if (option.isExpansionOption()) {
     96       return new ExpansionOptionValueDescription(option, optionsData);
     97     } else if (option.allowsMultiple()) {
     98       return new RepeatableOptionValueDescription(option);
     99     } else if (option.hasImplicitRequirements()) {
    100       return new OptionWithImplicitRequirementsValueDescription(option);
    101     } else {
    102       return new SingleOptionValueDescription(option);
    103     }
    104   }
    105 
    106   /**
    107    * For options that have not been set, this will return a correct OptionValueDescription for the
    108    * default value.
    109    */
    110   public static OptionValueDescription getDefaultOptionValue(OptionDefinition option) {
    111     return new DefaultOptionValueDescription(option);
    112   }
    113 
    114   private static class DefaultOptionValueDescription extends OptionValueDescription {
    115 
    116     private DefaultOptionValueDescription(OptionDefinition optionDefinition) {
    117       super(optionDefinition);
    118     }
    119 
    120     @Override
    121     public Object getValue() {
    122       return optionDefinition.getDefaultValue();
    123     }
    124 
    125     @Override
    126     public String getSourceString() {
    127       return null;
    128     }
    129 
    130     @Override
    131     ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings) {
    132       throw new IllegalStateException(
    133           "Cannot add values to the default option value. Create a modifiable "
    134               + "OptionValueDescription using createOptionValueDescription() instead.");
    135     }
    136 
    137     @Override
    138     public ImmutableList<ParsedOptionDescription> getCanonicalInstances() {
    139       return null;
    140     }
    141   }
    142 
    143   /**
    144    * The form of a value for a default type of flag, one that does not accumulate multiple values
    145    * and has no expansion.
    146    */
    147   private static class SingleOptionValueDescription extends OptionValueDescription {
    148     private ParsedOptionDescription effectiveOptionInstance;
    149     private Object effectiveValue;
    150 
    151     private SingleOptionValueDescription(OptionDefinition optionDefinition) {
    152       super(optionDefinition);
    153       if (optionDefinition.allowsMultiple()) {
    154         throw new ConstructionException("Can't have a single value for an allowMultiple option.");
    155       }
    156       if (optionDefinition.isExpansionOption()) {
    157         throw new ConstructionException("Can't have a single value for an expansion option.");
    158       }
    159       effectiveOptionInstance = null;
    160       effectiveValue = null;
    161     }
    162 
    163     @Override
    164     public Object getValue() {
    165       return effectiveValue;
    166     }
    167 
    168     @Override
    169     public String getSourceString() {
    170       return effectiveOptionInstance.getSource();
    171     }
    172 
    173     // Warnings should not end with a '.' because the internal reporter adds one automatically.
    174     @Override
    175     ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings)
    176         throws OptionsParsingException {
    177       // This might be the first value, in that case, just store it!
    178       if (effectiveOptionInstance == null) {
    179         effectiveOptionInstance = parsedOption;
    180         effectiveValue = effectiveOptionInstance.getConvertedValue();
    181         return null;
    182       }
    183 
    184       // If there was another value, check whether the new one will override it, and if so,
    185       // log warnings describing the change.
    186       if (parsedOption.getPriority().compareTo(effectiveOptionInstance.getPriority()) >= 0) {
    187         // Identify the option that might have led to the current and new value of this option.
    188         ParsedOptionDescription implicitDependent = parsedOption.getImplicitDependent();
    189         ParsedOptionDescription expandedFrom = parsedOption.getExpandedFrom();
    190         ParsedOptionDescription optionThatDependsOnEffectiveValue =
    191             effectiveOptionInstance.getImplicitDependent();
    192         ParsedOptionDescription optionThatExpandedToEffectiveValue =
    193             effectiveOptionInstance.getExpandedFrom();
    194 
    195         Object newValue = parsedOption.getConvertedValue();
    196         // Output warnings if there is conflicting options set different values in a way that might
    197         // not have been obvious to the user, such as through expansions and implicit requirements.
    198         if (!effectiveValue.equals(newValue)) {
    199           boolean samePriorityCategory =
    200               parsedOption
    201                   .getPriority()
    202                   .getPriorityCategory()
    203                   .equals(effectiveOptionInstance.getPriority().getPriorityCategory());
    204           if ((implicitDependent != null) && (optionThatDependsOnEffectiveValue != null)) {
    205             if (!implicitDependent.equals(optionThatDependsOnEffectiveValue)) {
    206               warnings.add(
    207                   String.format(
    208                       "%s is implicitly defined by both %s and %s",
    209                       optionDefinition, optionThatDependsOnEffectiveValue, implicitDependent));
    210             }
    211           } else if ((implicitDependent != null) && samePriorityCategory) {
    212             warnings.add(
    213                 String.format(
    214                     "%s is implicitly defined by %s; the implicitly set value "
    215                         + "overrides the previous one",
    216                     optionDefinition, implicitDependent));
    217           } else if (optionThatDependsOnEffectiveValue != null) {
    218             warnings.add(
    219                 String.format(
    220                     "A new value for %s overrides a previous implicit setting of that "
    221                         + "option by %s",
    222                     optionDefinition, optionThatDependsOnEffectiveValue));
    223           } else if (samePriorityCategory
    224               && ((optionThatExpandedToEffectiveValue == null) && (expandedFrom != null))) {
    225             // Create a warning if an expansion option overrides an explicit option:
    226             warnings.add(
    227                 String.format(
    228                     "%s was expanded and now overrides the explicit option %s with %s",
    229                     expandedFrom,
    230                     effectiveOptionInstance.getCommandLineForm(),
    231                     parsedOption.getCommandLineForm()));
    232           } else if ((optionThatExpandedToEffectiveValue != null) && (expandedFrom != null)) {
    233             warnings.add(
    234                 String.format(
    235                     "%s was expanded to from both %s and %s",
    236                     optionDefinition, optionThatExpandedToEffectiveValue, expandedFrom));
    237           }
    238         }
    239 
    240         // Record the new value:
    241         effectiveOptionInstance = parsedOption;
    242         effectiveValue = newValue;
    243       }
    244       return null;
    245     }
    246 
    247     @Override
    248     public ImmutableList<ParsedOptionDescription> getCanonicalInstances() {
    249       // If the current option is an implicit requirement, we don't need to list this value since
    250       // the parent implies it. In this case, it is sufficient to not list this value at all.
    251       if (effectiveOptionInstance.getImplicitDependent() == null) {
    252         return ImmutableList.of(effectiveOptionInstance);
    253       }
    254       return ImmutableList.of();
    255     }
    256   }
    257 
    258   /** The form of a value for an option that accumulates multiple values on the command line. */
    259   private static class RepeatableOptionValueDescription extends OptionValueDescription {
    260     ListMultimap<OptionPriority, ParsedOptionDescription> parsedOptions;
    261     ListMultimap<OptionPriority, Object> optionValues;
    262 
    263     private RepeatableOptionValueDescription(OptionDefinition optionDefinition) {
    264       super(optionDefinition);
    265       if (!optionDefinition.allowsMultiple()) {
    266         throw new ConstructionException(
    267             "Can't have a repeated value for a non-allowMultiple option.");
    268       }
    269       parsedOptions = ArrayListMultimap.create();
    270       optionValues = ArrayListMultimap.create();
    271     }
    272 
    273     @Override
    274     public String getSourceString() {
    275       return parsedOptions
    276           .asMap()
    277           .entrySet()
    278           .stream()
    279           .sorted(Comparator.comparing(Map.Entry::getKey))
    280           .map(Map.Entry::getValue)
    281           .flatMap(Collection::stream)
    282           .map(ParsedOptionDescription::getSource)
    283           .distinct()
    284           .collect(Collectors.joining(", "));
    285     }
    286 
    287     @Override
    288     public List<Object> getValue() {
    289       // Sort the results by option priority and return them in a new list. The generic type of
    290       // the list is not known at runtime, so we can't use it here.
    291       return optionValues
    292           .asMap()
    293           .entrySet()
    294           .stream()
    295           .sorted(Comparator.comparing(Map.Entry::getKey))
    296           .map(Map.Entry::getValue)
    297           .flatMap(Collection::stream)
    298           .collect(Collectors.toList());
    299     }
    300 
    301     @Override
    302     ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings)
    303         throws OptionsParsingException {
    304       // For repeatable options, we allow flags that take both single values and multiple values,
    305       // potentially collapsing them down.
    306       Object convertedValue = parsedOption.getConvertedValue();
    307       OptionPriority priority = parsedOption.getPriority();
    308       parsedOptions.put(priority, parsedOption);
    309       if (convertedValue instanceof List<?>) {
    310         optionValues.putAll(priority, (List<?>) convertedValue);
    311       } else {
    312         optionValues.put(priority, convertedValue);
    313       }
    314       return null;
    315     }
    316 
    317     @Override
    318     public ImmutableList<ParsedOptionDescription> getCanonicalInstances() {
    319       return parsedOptions
    320           .asMap()
    321           .entrySet()
    322           .stream()
    323           .sorted(Comparator.comparing(Map.Entry::getKey))
    324           .map(Map.Entry::getValue)
    325           .flatMap(Collection::stream)
    326           // Only provide the options that aren't implied elsewhere.
    327           .filter(optionDesc -> optionDesc.getImplicitDependent() == null)
    328           .collect(ImmutableList.toImmutableList());
    329     }
    330   }
    331 
    332   /**
    333    * The form of a value for an expansion option, one that does not have its own value but expands
    334    * in place to other options. This should be used for both flags with a static expansion defined
    335    * in {@link Option#expansion()} and flags with an {@link Option#expansionFunction()}.
    336    */
    337   private static class ExpansionOptionValueDescription extends OptionValueDescription {
    338     private final List<String> expansion;
    339 
    340     private ExpansionOptionValueDescription(
    341         OptionDefinition optionDefinition, OptionsData optionsData) {
    342       super(optionDefinition);
    343       this.expansion = optionsData.getEvaluatedExpansion(optionDefinition);
    344       if (!optionDefinition.isExpansionOption()) {
    345         throw new ConstructionException(
    346             "Options without expansions can't be tracked using ExpansionOptionValueDescription");
    347       }
    348     }
    349 
    350     @Override
    351     public Object getValue() {
    352       return null;
    353     }
    354 
    355     @Override
    356     public String getSourceString() {
    357       return null;
    358     }
    359 
    360     @Override
    361     ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings) {
    362       if (parsedOption.getUnconvertedValue() != null
    363           && !parsedOption.getUnconvertedValue().isEmpty()) {
    364         warnings.add(
    365             String.format(
    366                 "%s is an expansion option. It does not accept values, and does not change its "
    367                     + "expansion based on the value provided. Value '%s' will be ignored.",
    368                 optionDefinition, parsedOption.getUnconvertedValue()));
    369       }
    370 
    371       return new ExpansionBundle(
    372           expansion,
    373           (parsedOption.getSource() == null)
    374               ? String.format("expanded from %s", optionDefinition)
    375               : String.format(
    376                   "expanded from %s (source %s)", optionDefinition, parsedOption.getSource()));
    377     }
    378 
    379     @Override
    380     public ImmutableList<ParsedOptionDescription> getCanonicalInstances() {
    381       // The options this expands to are incorporated in their own right - this option does
    382       // not have a canonical form.
    383       return ImmutableList.of();
    384     }
    385   }
    386 
    387   /** The form of a value for a flag with implicit requirements. */
    388   private static class OptionWithImplicitRequirementsValueDescription
    389       extends SingleOptionValueDescription {
    390 
    391     private OptionWithImplicitRequirementsValueDescription(OptionDefinition optionDefinition) {
    392       super(optionDefinition);
    393       if (!optionDefinition.hasImplicitRequirements()) {
    394         throw new ConstructionException(
    395             "Options without implicit requirements can't be tracked using "
    396                 + "OptionWithImplicitRequirementsValueDescription");
    397       }
    398     }
    399 
    400     @Override
    401     ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings)
    402         throws OptionsParsingException {
    403       // This is a valued flag, its value is handled the same way as a normal
    404       // SingleOptionValueDescription. (We check at compile time that these flags aren't
    405       // "allowMultiple")
    406       ExpansionBundle superExpansion = super.addOptionInstance(parsedOption, warnings);
    407       Preconditions.checkArgument(
    408           superExpansion == null, "SingleOptionValueDescription should not expand to anything.");
    409       if (parsedOption.getConvertedValue().equals(optionDefinition.getDefaultValue())) {
    410         warnings.add(
    411             String.format(
    412                 "%s sets %s to its default value. Since this option has implicit requirements that "
    413                     + "are set whenever the option is explicitly provided, regardless of the "
    414                     + "value, this will behave differently than letting a default be a default. "
    415                     + "Specifically, this options expands to {%s}.",
    416                 parsedOption.getCommandLineForm(),
    417                 optionDefinition,
    418                 String.join(" ", optionDefinition.getImplicitRequirements())));
    419       }
    420 
    421       // Now deal with the implicit requirements.
    422       return new ExpansionBundle(
    423           ImmutableList.copyOf(optionDefinition.getImplicitRequirements()),
    424           (parsedOption.getSource() == null)
    425               ? String.format("implicit requirement of %s", optionDefinition)
    426               : String.format(
    427                   "implicit requirement of %s (source %s)",
    428                   optionDefinition, parsedOption.getSource()));
    429     }
    430   }
    431 }
    432 
    433 
    434