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