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 
OptionValueDescription(OptionDefinition optionDefinition)38   public OptionValueDescription(OptionDefinition optionDefinition) {
39     this.optionDefinition = optionDefinition;
40   }
41 
getOptionDefinition()42   public OptionDefinition getOptionDefinition() {
43     return optionDefinition;
44   }
45 
46   /** Returns the current or final value of this option. */
getValue()47   public abstract Object getValue();
48 
49   /** Returns the source(s) of this option, if there were multiple, duplicates are removed. */
getSourceString()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    */
addOptionInstance( ParsedOptionDescription parsedOption, List<String> warnings)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 
ExpansionBundle(List<String> args, String source)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
getCanonicalInstances()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    */
createOptionValueDescription( OptionDefinition option, OptionsData optionsData)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    */
getDefaultOptionValue(OptionDefinition option)110   public static OptionValueDescription getDefaultOptionValue(OptionDefinition option) {
111     return new DefaultOptionValueDescription(option);
112   }
113 
114   private static class DefaultOptionValueDescription extends OptionValueDescription {
115 
DefaultOptionValueDescription(OptionDefinition optionDefinition)116     private DefaultOptionValueDescription(OptionDefinition optionDefinition) {
117       super(optionDefinition);
118     }
119 
120     @Override
getValue()121     public Object getValue() {
122       return optionDefinition.getDefaultValue();
123     }
124 
125     @Override
getSourceString()126     public String getSourceString() {
127       return null;
128     }
129 
130     @Override
addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings)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
getCanonicalInstances()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 
SingleOptionValueDescription(OptionDefinition optionDefinition)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
getValue()164     public Object getValue() {
165       return effectiveValue;
166     }
167 
168     @Override
getSourceString()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
addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings)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
getCanonicalInstances()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 
RepeatableOptionValueDescription(OptionDefinition optionDefinition)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
getSourceString()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
getValue()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
addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings)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
getCanonicalInstances()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 
ExpansionOptionValueDescription( OptionDefinition optionDefinition, OptionsData optionsData)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
getValue()351     public Object getValue() {
352       return null;
353     }
354 
355     @Override
getSourceString()356     public String getSourceString() {
357       return null;
358     }
359 
360     @Override
addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings)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
getCanonicalInstances()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 
OptionWithImplicitRequirementsValueDescription(OptionDefinition optionDefinition)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
addOptionInstance(ParsedOptionDescription parsedOption, List<String> warnings)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