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.Entry;
     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 if (option.isWrapperOption()) {
    102       return new WrapperOptionValueDescription(option);
    103     } else {
    104       return new SingleOptionValueDescription(option);
    105     }
    106   }
    107 
    108   /**
    109    * For options that have not been set, this will return a correct OptionValueDescription for the
    110    * default value.
    111    */
    112   public static OptionValueDescription getDefaultOptionValue(OptionDefinition option) {
    113     return new DefaultOptionValueDescription(option);
    114   }
    115 
    116   private static class DefaultOptionValueDescription extends OptionValueDescription {
    117 
    118     private DefaultOptionValueDescription(OptionDefinition optionDefinition) {
    119       super(optionDefinition);
    120     }
    121 
    122     @Override
    123     public Object getValue() {
    124       return optionDefinition.getDefaultValue();
    125     }
    126 
    127     @Override
    128     public String getSourceString() {
    129       return null;
    130     }
    131 
    132     @Override
    133     ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings) {
    134       throw new IllegalStateException(
    135           "Cannot add values to the default option value. Create a modifiable "
    136               + "OptionValueDescription using createOptionValueDescription() instead.");
    137     }
    138 
    139     @Override
    140     public ImmutableList<ParsedOptionDescription> getCanonicalInstances() {
    141       return null;
    142     }
    143   }
    144 
    145   /**
    146    * The form of a value for a default type of flag, one that does not accumulate multiple values
    147    * and has no expansion.
    148    */
    149   private static class SingleOptionValueDescription extends OptionValueDescription {
    150     private ParsedOptionDescription effectiveOptionInstance;
    151     private Object effectiveValue;
    152 
    153     private SingleOptionValueDescription(OptionDefinition optionDefinition) {
    154       super(optionDefinition);
    155       if (optionDefinition.allowsMultiple()) {
    156         throw new ConstructionException("Can't have a single value for an allowMultiple option.");
    157       }
    158       if (optionDefinition.isExpansionOption()) {
    159         throw new ConstructionException("Can't have a single value for an expansion option.");
    160       }
    161       effectiveOptionInstance = null;
    162       effectiveValue = null;
    163     }
    164 
    165     @Override
    166     public Object getValue() {
    167       return effectiveValue;
    168     }
    169 
    170     @Override
    171     public String getSourceString() {
    172       return effectiveOptionInstance.getSource();
    173     }
    174 
    175     // Warnings should not end with a '.' because the internal reporter adds one automatically.
    176     @Override
    177     ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings)
    178         throws OptionsParsingException {
    179       // This might be the first value, in that case, just store it!
    180       if (effectiveOptionInstance == null) {
    181         effectiveOptionInstance = parsedOption;
    182         effectiveValue = effectiveOptionInstance.getConvertedValue();
    183         return null;
    184       }
    185 
    186       // If there was another value, check whether the new one will override it, and if so,
    187       // log warnings describing the change.
    188       if (parsedOption.getPriority().compareTo(effectiveOptionInstance.getPriority()) >= 0) {
    189         // Identify the option that might have led to the current and new value of this option.
    190         OptionDefinition implicitDependent = parsedOption.getImplicitDependent();
    191         OptionDefinition expandedFrom = parsedOption.getExpandedFrom();
    192         OptionDefinition optionThatDependsOnEffectiveValue =
    193             effectiveOptionInstance.getImplicitDependent();
    194         OptionDefinition optionThatExpandedToEffectiveValue =
    195             effectiveOptionInstance.getExpandedFrom();
    196 
    197         Object newValue = parsedOption.getConvertedValue();
    198         // Output warnings if there is conflicting options set different values in a way that might
    199         // not have been obvious to the user, such as through expansions and implicit requirements.
    200         if (!effectiveValue.equals(newValue)) {
    201           boolean samePriorityCategory =
    202               parsedOption
    203                   .getPriority()
    204                   .getPriorityCategory()
    205                   .equals(effectiveOptionInstance.getPriority().getPriorityCategory());
    206           if ((implicitDependent != null) && (optionThatDependsOnEffectiveValue != null)) {
    207             if (!implicitDependent.equals(optionThatDependsOnEffectiveValue)) {
    208               warnings.add(
    209                   String.format(
    210                       "%s is implicitly defined by both %s and %s",
    211                       optionDefinition, optionThatDependsOnEffectiveValue, implicitDependent));
    212             }
    213           } else if ((implicitDependent != null) && samePriorityCategory) {
    214             warnings.add(
    215                 String.format(
    216                     "%s is implicitly defined by %s; the implicitly set value "
    217                         + "overrides the previous one",
    218                     optionDefinition, implicitDependent));
    219           } else if (optionThatDependsOnEffectiveValue != null) {
    220             warnings.add(
    221                 String.format(
    222                     "A new value for %s overrides a previous implicit setting of that "
    223                         + "option by %s",
    224                     optionDefinition, optionThatDependsOnEffectiveValue));
    225           } else if (samePriorityCategory
    226               && ((optionThatExpandedToEffectiveValue == null) && (expandedFrom != null))) {
    227             // Create a warning if an expansion option overrides an explicit option:
    228             warnings.add(
    229                 String.format(
    230                     "%s was expanded and now overrides a previous explicitly specified %s with %s",
    231                     expandedFrom,
    232                     effectiveOptionInstance.getCommandLineForm(),
    233                     parsedOption.getCommandLineForm()));
    234           } else if ((optionThatExpandedToEffectiveValue != null) && (expandedFrom != null)) {
    235             warnings.add(
    236                 String.format(
    237                     "%s was expanded to from both %s and %s",
    238                     optionDefinition, optionThatExpandedToEffectiveValue, expandedFrom));
    239           }
    240         }
    241 
    242         // Record the new value:
    243         effectiveOptionInstance = parsedOption;
    244         effectiveValue = newValue;
    245       }
    246       return null;
    247     }
    248 
    249     @Override
    250     public ImmutableList<ParsedOptionDescription> getCanonicalInstances() {
    251       // If the current option is an implicit requirement, we don't need to list this value since
    252       // the parent implies it. In this case, it is sufficient to not list this value at all.
    253       if (effectiveOptionInstance.getImplicitDependent() == null) {
    254         return ImmutableList.of(effectiveOptionInstance);
    255       }
    256       return ImmutableList.of();
    257     }
    258   }
    259 
    260   /** The form of a value for an option that accumulates multiple values on the command line. */
    261   private static class RepeatableOptionValueDescription extends OptionValueDescription {
    262     ListMultimap<OptionPriority, ParsedOptionDescription> parsedOptions;
    263     ListMultimap<OptionPriority, Object> optionValues;
    264 
    265     private RepeatableOptionValueDescription(OptionDefinition optionDefinition) {
    266       super(optionDefinition);
    267       if (!optionDefinition.allowsMultiple()) {
    268         throw new ConstructionException(
    269             "Can't have a repeated value for a non-allowMultiple option.");
    270       }
    271       parsedOptions = ArrayListMultimap.create();
    272       optionValues = ArrayListMultimap.create();
    273     }
    274 
    275     @Override
    276     public String getSourceString() {
    277       return parsedOptions
    278           .asMap()
    279           .entrySet()
    280           .stream()
    281           .sorted(Comparator.comparing(Entry::getKey))
    282           .map(Entry::getValue)
    283           .flatMap(Collection::stream)
    284           .map(ParsedOptionDescription::getSource)
    285           .distinct()
    286           .collect(Collectors.joining(", "));
    287     }
    288 
    289     @Override
    290     public List<Object> getValue() {
    291       // Sort the results by option priority and return them in a new list. The generic type of
    292       // the list is not known at runtime, so we can't use it here.
    293       return optionValues
    294           .asMap()
    295           .entrySet()
    296           .stream()
    297           .sorted(Comparator.comparing(Entry::getKey))
    298           .map(Entry::getValue)
    299           .flatMap(Collection::stream)
    300           .collect(Collectors.toList());
    301     }
    302 
    303     @Override
    304     ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings)
    305         throws OptionsParsingException {
    306       // For repeatable options, we allow flags that take both single values and multiple values,
    307       // potentially collapsing them down.
    308       Object convertedValue = parsedOption.getConvertedValue();
    309       OptionPriority priority = parsedOption.getPriority();
    310       parsedOptions.put(priority, parsedOption);
    311       if (convertedValue instanceof List<?>) {
    312         optionValues.putAll(priority, (List<?>) convertedValue);
    313       } else {
    314         optionValues.put(priority, convertedValue);
    315       }
    316       return null;
    317     }
    318 
    319     @Override
    320     public ImmutableList<ParsedOptionDescription> getCanonicalInstances() {
    321       return parsedOptions
    322           .asMap()
    323           .entrySet()
    324           .stream()
    325           .sorted(Comparator.comparing(Entry::getKey))
    326           .map(Entry::getValue)
    327           .flatMap(Collection::stream)
    328           // Only provide the options that aren't implied elsewhere.
    329           .filter(optionDesc -> optionDesc.getImplicitDependent() == null)
    330           .collect(ImmutableList.toImmutableList());
    331     }
    332   }
    333 
    334   /**
    335    * The form of a value for an expansion option, one that does not have its own value but expands
    336    * in place to other options. This should be used for both flags with a static expansion defined
    337    * in {@link Option#expansion()} and flags with an {@link Option#expansionFunction()}.
    338    */
    339   private static class ExpansionOptionValueDescription extends OptionValueDescription {
    340     private final List<String> expansion;
    341 
    342     private ExpansionOptionValueDescription(
    343         OptionDefinition optionDefinition, OptionsData optionsData) {
    344       super(optionDefinition);
    345       this.expansion = optionsData.getEvaluatedExpansion(optionDefinition);
    346       if (!optionDefinition.isExpansionOption()) {
    347         throw new ConstructionException(
    348             "Options without expansions can't be tracked using ExpansionOptionValueDescription");
    349       }
    350     }
    351 
    352     @Override
    353     public Object getValue() {
    354       return null;
    355     }
    356 
    357     @Override
    358     public String getSourceString() {
    359       return null;
    360     }
    361 
    362     @Override
    363     ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings) {
    364       if (parsedOption.getUnconvertedValue() != null
    365           && !parsedOption.getUnconvertedValue().isEmpty()) {
    366         warnings.add(
    367             String.format(
    368                 "%s is an expansion option. It does not accept values, and does not change its "
    369                     + "expansion based on the value provided. Value '%s' will be ignored.",
    370                 optionDefinition, parsedOption.getUnconvertedValue()));
    371       }
    372 
    373       return new ExpansionBundle(
    374           expansion,
    375           (parsedOption.getSource() == null)
    376               ? String.format("expanded from %s", optionDefinition)
    377               : String.format(
    378                   "expanded from %s (source %s)", optionDefinition, parsedOption.getSource()));
    379     }
    380 
    381     @Override
    382     public ImmutableList<ParsedOptionDescription> getCanonicalInstances() {
    383       // The options this expands to are incorporated in their own right - this option does
    384       // not have a canonical form.
    385       return ImmutableList.of();
    386     }
    387   }
    388 
    389   /** The form of a value for a flag with implicit requirements. */
    390   private static class OptionWithImplicitRequirementsValueDescription
    391       extends SingleOptionValueDescription {
    392 
    393     private OptionWithImplicitRequirementsValueDescription(OptionDefinition optionDefinition) {
    394       super(optionDefinition);
    395       if (!optionDefinition.hasImplicitRequirements()) {
    396         throw new ConstructionException(
    397             "Options without implicit requirements can't be tracked using "
    398                 + "OptionWithImplicitRequirementsValueDescription");
    399       }
    400     }
    401 
    402     @Override
    403     ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings)
    404         throws OptionsParsingException {
    405       // This is a valued flag, its value is handled the same way as a normal
    406       // SingleOptionValueDescription. (We check at compile time that these flags aren't
    407       // "allowMultiple")
    408       ExpansionBundle superExpansion = super.addOptionInstance(parsedOption, warnings);
    409       Preconditions.checkArgument(
    410           superExpansion == null, "SingleOptionValueDescription should not expand to anything.");
    411       if (parsedOption.getConvertedValue().equals(optionDefinition.getDefaultValue())) {
    412         warnings.add(
    413             String.format(
    414                 "%s sets %s to its default value. Since this option has implicit requirements that "
    415                     + "are set whenever the option is explicitly provided, regardless of the "
    416                     + "value, this will behave differently than letting a default be a default. "
    417                     + "Specifically, this options expands to {%s}.",
    418                 parsedOption.getCommandLineForm(),
    419                 optionDefinition,
    420                 String.join(" ", optionDefinition.getImplicitRequirements())));
    421       }
    422 
    423       // Now deal with the implicit requirements.
    424       return new ExpansionBundle(
    425           ImmutableList.copyOf(optionDefinition.getImplicitRequirements()),
    426           (parsedOption.getSource() == null)
    427               ? String.format("implicit requirement of %s", optionDefinition)
    428               : String.format(
    429                   "implicit requirement of %s (source %s)",
    430                   optionDefinition, parsedOption.getSource()));
    431     }
    432   }
    433 
    434   /** Form for options that contain other options in the value text to which they expand. */
    435   private static final class WrapperOptionValueDescription extends OptionValueDescription {
    436 
    437     WrapperOptionValueDescription(OptionDefinition optionDefinition) {
    438       super(optionDefinition);
    439     }
    440 
    441     @Override
    442     public Object getValue() {
    443       return null;
    444     }
    445 
    446     @Override
    447     public String getSourceString() {
    448       return null;
    449     }
    450 
    451     @Override
    452     ExpansionBundle addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings)
    453         throws OptionsParsingException {
    454       if (!parsedOption.getUnconvertedValue().startsWith("-")) {
    455         throw new OptionsParsingException(
    456             String.format(
    457                 "Invalid value format for %s. You may have meant --%s=--%s",
    458                 optionDefinition,
    459                 optionDefinition.getOptionName(),
    460                 parsedOption.getUnconvertedValue()));
    461       }
    462       return new ExpansionBundle(
    463           ImmutableList.of(parsedOption.getUnconvertedValue()),
    464           (parsedOption.getSource() == null)
    465               ? String.format("unwrapped from %s", optionDefinition)
    466               : String.format(
    467                   "unwrapped from %s (source %s)", optionDefinition, parsedOption.getSource()));
    468     }
    469 
    470     @Override
    471     public ImmutableList<ParsedOptionDescription> getCanonicalInstances() {
    472       // No wrapper options get listed in the canonical form - the options they are wrapping will
    473       // be in the right place.
    474       return ImmutableList.of();
    475     }
    476   }
    477 }
    478 
    479 
    480