1 // Copyright 2014 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 static java.util.Comparator.comparing; 18 import static java.util.stream.Collectors.toCollection; 19 20 import com.google.common.base.Joiner; 21 import com.google.common.base.Preconditions; 22 import com.google.common.collect.ImmutableList; 23 import com.google.common.collect.Iterators; 24 import com.google.devtools.common.options.OptionPriority.PriorityCategory; 25 import com.google.devtools.common.options.OptionValueDescription.ExpansionBundle; 26 import com.google.devtools.common.options.OptionsParser.OptionDescription; 27 import java.lang.reflect.Constructor; 28 import java.util.ArrayList; 29 import java.util.Collection; 30 import java.util.HashMap; 31 import java.util.Iterator; 32 import java.util.List; 33 import java.util.Map; 34 import java.util.function.Function; 35 import java.util.stream.Collectors; 36 import java.util.stream.Stream; 37 import javax.annotation.Nullable; 38 39 /** 40 * The implementation of the options parser. This is intentionally package 41 * private for full flexibility. Use {@link OptionsParser} or {@link Options} 42 * if you're a consumer. 43 */ 44 class OptionsParserImpl { 45 46 private final OptionsData optionsData; 47 48 /** 49 * We store the results of option parsing in here - since there can only be one value per option 50 * field, this is where the different instances of an option have been combined and the final 51 * value is tracked. It'll look like 52 * 53 * <pre> 54 * OptionDefinition("--host") -> "www.google.com" 55 * OptionDefinition("--port") -> 80 56 * </pre> 57 * 58 * This map is modified by repeated calls to {@link #parse(OptionPriority.PriorityCategory, 59 * Function,List)}. 60 */ 61 private final Map<OptionDefinition, OptionValueDescription> optionValues = new HashMap<>(); 62 63 /** 64 * Explicit option tracking, tracking each option as it was provided, after they have been parsed. 65 * 66 * <p>The value is unconverted, still the string as it was read from the input, or partially 67 * altered in cases where the flag was set by non {@code --flag=value} forms; e.g. {@code --nofoo} 68 * becomes {@code --foo=0}. 69 */ 70 private final List<ParsedOptionDescription> parsedOptions = new ArrayList<>(); 71 72 private final List<String> warnings = new ArrayList<>(); 73 74 /** 75 * Since parse() expects multiple calls to it with the same {@link PriorityCategory} to be treated 76 * as though the args in the later call have higher priority over the earlier calls, we need to 77 * track the high water mark of option priority at each category. Each call to parse will start at 78 * this level. 79 */ 80 private final Map<PriorityCategory, OptionPriority> nextPriorityPerPriorityCategory = 81 Stream.of(PriorityCategory.values()) 82 .collect(Collectors.toMap(p -> p, OptionPriority::lowestOptionPriorityAtCategory)); 83 84 private boolean allowSingleDashLongOptions = false; 85 86 private ArgsPreProcessor argsPreProcessor = args -> args; 87 88 /** Create a new parser object. Do not accept a null OptionsData object. */ OptionsParserImpl(OptionsData optionsData)89 OptionsParserImpl(OptionsData optionsData) { 90 Preconditions.checkNotNull(optionsData); 91 this.optionsData = optionsData; 92 } 93 getOptionsData()94 OptionsData getOptionsData() { 95 return optionsData; 96 } 97 98 /** 99 * Indicates whether or not the parser will allow long options with a 100 * single-dash, instead of the usual double-dash, too, eg. -example instead of just --example. 101 */ setAllowSingleDashLongOptions(boolean allowSingleDashLongOptions)102 void setAllowSingleDashLongOptions(boolean allowSingleDashLongOptions) { 103 this.allowSingleDashLongOptions = allowSingleDashLongOptions; 104 } 105 106 /** Sets the ArgsPreProcessor for manipulations of the options before parsing. */ setArgsPreProcessor(ArgsPreProcessor preProcessor)107 void setArgsPreProcessor(ArgsPreProcessor preProcessor) { 108 this.argsPreProcessor = Preconditions.checkNotNull(preProcessor); 109 } 110 111 /** Implements {@link OptionsParser#asCompleteListOfParsedOptions()}. */ asCompleteListOfParsedOptions()112 List<ParsedOptionDescription> asCompleteListOfParsedOptions() { 113 return parsedOptions 114 .stream() 115 // It is vital that this sort is stable so that options on the same priority are not 116 // reordered. 117 .sorted(comparing(ParsedOptionDescription::getPriority)) 118 .collect(toCollection(ArrayList::new)); 119 } 120 121 /** Implements {@link OptionsParser#asListOfExplicitOptions()}. */ asListOfExplicitOptions()122 List<ParsedOptionDescription> asListOfExplicitOptions() { 123 return parsedOptions 124 .stream() 125 .filter(ParsedOptionDescription::isExplicit) 126 // It is vital that this sort is stable so that options on the same priority are not 127 // reordered. 128 .sorted(comparing(ParsedOptionDescription::getPriority)) 129 .collect(toCollection(ArrayList::new)); 130 } 131 132 /** Implements {@link OptionsParser#canonicalize}. */ asCanonicalizedList()133 List<String> asCanonicalizedList() { 134 return asCanonicalizedListOfParsedOptions() 135 .stream() 136 .map(ParsedOptionDescription::getDeprecatedCanonicalForm) 137 .collect(ImmutableList.toImmutableList()); 138 } 139 140 /** Implements {@link OptionsParser#canonicalize}. */ asCanonicalizedListOfParsedOptions()141 List<ParsedOptionDescription> asCanonicalizedListOfParsedOptions() { 142 return optionValues 143 .keySet() 144 .stream() 145 .sorted() 146 .map(optionDefinition -> optionValues.get(optionDefinition).getCanonicalInstances()) 147 .flatMap(Collection::stream) 148 .collect(ImmutableList.toImmutableList()); 149 } 150 151 /** Implements {@link OptionsParser#asListOfOptionValues()}. */ asListOfEffectiveOptions()152 List<OptionValueDescription> asListOfEffectiveOptions() { 153 List<OptionValueDescription> result = new ArrayList<>(); 154 for (Map.Entry<String, OptionDefinition> mapEntry : optionsData.getAllOptionDefinitions()) { 155 OptionDefinition optionDefinition = mapEntry.getValue(); 156 OptionValueDescription optionValue = optionValues.get(optionDefinition); 157 if (optionValue == null) { 158 result.add(OptionValueDescription.getDefaultOptionValue(optionDefinition)); 159 } else { 160 result.add(optionValue); 161 } 162 } 163 return result; 164 } 165 maybeAddDeprecationWarning(OptionDefinition optionDefinition)166 private void maybeAddDeprecationWarning(OptionDefinition optionDefinition) { 167 // Continue to support the old behavior for @Deprecated options. 168 String warning = optionDefinition.getDeprecationWarning(); 169 if (!warning.isEmpty() || (optionDefinition.getField().isAnnotationPresent(Deprecated.class))) { 170 addDeprecationWarning(optionDefinition.getOptionName(), warning); 171 } 172 } 173 addDeprecationWarning(String optionName, String warning)174 private void addDeprecationWarning(String optionName, String warning) { 175 warnings.add( 176 String.format( 177 "Option '%s' is deprecated%s", optionName, (warning.isEmpty() ? "" : ": " + warning))); 178 } 179 180 clearValue(OptionDefinition optionDefinition)181 OptionValueDescription clearValue(OptionDefinition optionDefinition) 182 throws OptionsParsingException { 183 return optionValues.remove(optionDefinition); 184 } 185 getOptionValueDescription(String name)186 OptionValueDescription getOptionValueDescription(String name) { 187 OptionDefinition optionDefinition = optionsData.getOptionDefinitionFromName(name); 188 if (optionDefinition == null) { 189 throw new IllegalArgumentException("No such option '" + name + "'"); 190 } 191 return optionValues.get(optionDefinition); 192 } 193 getOptionDescription(String name)194 OptionDescription getOptionDescription(String name) throws OptionsParsingException { 195 OptionDefinition optionDefinition = optionsData.getOptionDefinitionFromName(name); 196 if (optionDefinition == null) { 197 return null; 198 } 199 return new OptionDescription(optionDefinition, optionsData); 200 } 201 202 /** 203 * Implementation of {@link OptionsParser#getExpansionValueDescriptions(OptionDefinition, 204 * OptionInstanceOrigin)} 205 */ getExpansionValueDescriptions( OptionDefinition expansionFlag, OptionInstanceOrigin originOfExpansionFlag)206 ImmutableList<ParsedOptionDescription> getExpansionValueDescriptions( 207 OptionDefinition expansionFlag, OptionInstanceOrigin originOfExpansionFlag) 208 throws OptionsParsingException { 209 ImmutableList.Builder<ParsedOptionDescription> builder = ImmutableList.builder(); 210 OptionInstanceOrigin originOfSubflags; 211 ImmutableList<String> options; 212 if (expansionFlag.hasImplicitRequirements()) { 213 options = ImmutableList.copyOf(expansionFlag.getImplicitRequirements()); 214 originOfSubflags = 215 new OptionInstanceOrigin( 216 originOfExpansionFlag.getPriority(), 217 String.format( 218 "implicitly required by %s (source: %s)", 219 expansionFlag, originOfExpansionFlag.getSource()), 220 expansionFlag, 221 null); 222 } else if (expansionFlag.isExpansionOption()) { 223 options = optionsData.getEvaluatedExpansion(expansionFlag); 224 originOfSubflags = 225 new OptionInstanceOrigin( 226 originOfExpansionFlag.getPriority(), 227 String.format( 228 "expanded by %s (source: %s)", expansionFlag, originOfExpansionFlag.getSource()), 229 null, 230 expansionFlag); 231 } else { 232 return ImmutableList.of(); 233 } 234 235 Iterator<String> optionsIterator = options.iterator(); 236 while (optionsIterator.hasNext()) { 237 String unparsedFlagExpression = optionsIterator.next(); 238 ParsedOptionDescription parsedOption = 239 identifyOptionAndPossibleArgument( 240 unparsedFlagExpression, 241 optionsIterator, 242 originOfSubflags.getPriority(), 243 o -> originOfSubflags.getSource(), 244 originOfSubflags.getImplicitDependent(), 245 originOfSubflags.getExpandedFrom()); 246 builder.add(parsedOption); 247 } 248 return builder.build(); 249 } 250 containsExplicitOption(String name)251 boolean containsExplicitOption(String name) { 252 OptionDefinition optionDefinition = optionsData.getOptionDefinitionFromName(name); 253 if (optionDefinition == null) { 254 throw new IllegalArgumentException("No such option '" + name + "'"); 255 } 256 return optionValues.get(optionDefinition) != null; 257 } 258 259 /** 260 * Parses the args, and returns what it doesn't parse. May be called multiple times, and may be 261 * called recursively. The option's definition dictates how it reacts to multiple settings. By 262 * default, the arg seen last at the highest priority takes precedence, overriding the early 263 * values. Options that accumulate multiple values will track them in priority and appearance 264 * order. 265 */ parse( PriorityCategory priorityCat, Function<OptionDefinition, String> sourceFunction, List<String> args)266 List<String> parse( 267 PriorityCategory priorityCat, 268 Function<OptionDefinition, String> sourceFunction, 269 List<String> args) 270 throws OptionsParsingException { 271 ResidueAndPriority residueAndPriority = 272 parse(nextPriorityPerPriorityCategory.get(priorityCat), sourceFunction, null, null, args); 273 nextPriorityPerPriorityCategory.put(priorityCat, residueAndPriority.nextPriority); 274 return residueAndPriority.residue; 275 } 276 277 private static final class ResidueAndPriority { 278 List<String> residue; 279 OptionPriority nextPriority; 280 ResidueAndPriority(List<String> residue, OptionPriority nextPriority)281 public ResidueAndPriority(List<String> residue, OptionPriority nextPriority) { 282 this.residue = residue; 283 this.nextPriority = nextPriority; 284 } 285 } 286 287 /** Parses the args at the fixed priority. */ parseOptionsFixedAtSpecificPriority( OptionPriority priority, Function<OptionDefinition, String> sourceFunction, List<String> args)288 List<String> parseOptionsFixedAtSpecificPriority( 289 OptionPriority priority, Function<OptionDefinition, String> sourceFunction, List<String> args) 290 throws OptionsParsingException { 291 ResidueAndPriority residueAndPriority = 292 parse(OptionPriority.getLockedPriority(priority), sourceFunction, null, null, args); 293 return residueAndPriority.residue; 294 } 295 296 /** 297 * Parses the args, and returns what it doesn't parse. May be called multiple times, and may be 298 * called recursively. Calls may contain intersecting sets of options; in that case, the arg seen 299 * last takes precedence. 300 * 301 * <p>The method treats options that have neither an implicitDependent nor an expandedFrom value 302 * as explicitly set. 303 */ parse( OptionPriority priority, Function<OptionDefinition, String> sourceFunction, OptionDefinition implicitDependent, OptionDefinition expandedFrom, List<String> args)304 private ResidueAndPriority parse( 305 OptionPriority priority, 306 Function<OptionDefinition, String> sourceFunction, 307 OptionDefinition implicitDependent, 308 OptionDefinition expandedFrom, 309 List<String> args) 310 throws OptionsParsingException { 311 List<String> unparsedArgs = new ArrayList<>(); 312 313 Iterator<String> argsIterator = argsPreProcessor.preProcess(args).iterator(); 314 while (argsIterator.hasNext()) { 315 String arg = argsIterator.next(); 316 317 if (!arg.startsWith("-")) { 318 unparsedArgs.add(arg); 319 continue; // not an option arg 320 } 321 322 if (arg.equals("--")) { // "--" means all remaining args aren't options 323 Iterators.addAll(unparsedArgs, argsIterator); 324 break; 325 } 326 327 ParsedOptionDescription parsedOption = 328 identifyOptionAndPossibleArgument( 329 arg, argsIterator, priority, sourceFunction, implicitDependent, expandedFrom); 330 handleNewParsedOption(parsedOption); 331 priority = OptionPriority.nextOptionPriority(priority); 332 } 333 334 // Go through the final values and make sure they are valid values for their option. Unlike any 335 // checks that happened above, this also checks that flags that were not set have a valid 336 // default value. getValue() will throw if the value is invalid. 337 for (OptionValueDescription valueDescription : asListOfEffectiveOptions()) { 338 valueDescription.getValue(); 339 } 340 341 return new ResidueAndPriority(unparsedArgs, priority); 342 } 343 344 /** 345 * Implementation of {@link OptionsParser#addOptionValueAtSpecificPriority(OptionInstanceOrigin, 346 * OptionDefinition, String)} 347 */ addOptionValueAtSpecificPriority( OptionInstanceOrigin origin, OptionDefinition option, String unconvertedValue)348 void addOptionValueAtSpecificPriority( 349 OptionInstanceOrigin origin, OptionDefinition option, String unconvertedValue) 350 throws OptionsParsingException { 351 Preconditions.checkNotNull(option); 352 Preconditions.checkNotNull( 353 unconvertedValue, 354 "Cannot set %s to a null value. Pass \"\" if an empty value is required.", 355 option); 356 Preconditions.checkNotNull( 357 origin, 358 "Cannot assign value \'%s\' to %s without a clear origin for this value.", 359 unconvertedValue, 360 option); 361 PriorityCategory priorityCategory = origin.getPriority().getPriorityCategory(); 362 boolean isNotDefault = priorityCategory != OptionPriority.PriorityCategory.DEFAULT; 363 Preconditions.checkArgument( 364 isNotDefault, 365 "Attempt to assign value \'%s\' to %s at priority %s failed. Cannot set options at " 366 + "default priority - by definition, that means the option is unset.", 367 unconvertedValue, 368 option, 369 priorityCategory); 370 371 handleNewParsedOption( 372 new ParsedOptionDescription( 373 option, 374 String.format("--%s=%s", option.getOptionName(), unconvertedValue), 375 unconvertedValue, 376 origin)); 377 } 378 379 /** Takes care of tracking the parsed option's value in relation to other options. */ handleNewParsedOption(ParsedOptionDescription parsedOption)380 private void handleNewParsedOption(ParsedOptionDescription parsedOption) 381 throws OptionsParsingException { 382 OptionDefinition optionDefinition = parsedOption.getOptionDefinition(); 383 // All options can be deprecated; check and warn before doing any option-type specific work. 384 maybeAddDeprecationWarning(optionDefinition); 385 // Track the value, before any remaining option-type specific work that is done outside of 386 // the OptionValueDescription. 387 OptionValueDescription entry = 388 optionValues.computeIfAbsent( 389 optionDefinition, 390 def -> OptionValueDescription.createOptionValueDescription(def, optionsData)); 391 ExpansionBundle expansionBundle = entry.addOptionInstance(parsedOption, warnings); 392 @Nullable String unconvertedValue = parsedOption.getUnconvertedValue(); 393 394 // There are 3 types of flags that expand to other flag values. Expansion flags are the 395 // accepted way to do this, but two legacy features remain: implicit requirements and wrapper 396 // options. We rely on the OptionProcessor compile-time check's guarantee that no option sets 397 // multiple of these behaviors. (In Bazel, --config is another such flag, but that expansion 398 // is not controlled within the options parser, so we ignore it here) 399 400 // As much as possible, we want the behaviors of these different types of flags to be 401 // identical, as this minimizes the number of edge cases, but we do not yet track these values 402 // in the same way. Wrapper options are replaced by their value and implicit requirements are 403 // hidden from the reported lists of parsed options. 404 if (parsedOption.getImplicitDependent() == null && !optionDefinition.isWrapperOption()) { 405 // Log explicit options and expanded options in the order they are parsed (can be sorted 406 // later). This information is needed to correctly canonicalize flags. 407 parsedOptions.add(parsedOption); 408 } 409 410 if (expansionBundle != null) { 411 ResidueAndPriority residueAndPriority = 412 parse( 413 OptionPriority.getLockedPriority(parsedOption.getPriority()), 414 o -> expansionBundle.sourceOfExpansionArgs, 415 optionDefinition.hasImplicitRequirements() ? optionDefinition : null, 416 optionDefinition.isExpansionOption() ? optionDefinition : null, 417 expansionBundle.expansionArgs); 418 if (!residueAndPriority.residue.isEmpty()) { 419 if (optionDefinition.isWrapperOption()) { 420 throw new OptionsParsingException( 421 "Unparsed options remain after unwrapping " 422 + unconvertedValue 423 + ": " 424 + Joiner.on(' ').join(residueAndPriority.residue)); 425 } else { 426 // Throw an assertion here, because this indicates an error in the definition of this 427 // option's expansion or requirements, not with the input as provided by the user. 428 throw new AssertionError( 429 "Unparsed options remain after processing " 430 + unconvertedValue 431 + ": " 432 + Joiner.on(' ').join(residueAndPriority.residue)); 433 } 434 } 435 } 436 } 437 identifyOptionAndPossibleArgument( String arg, Iterator<String> nextArgs, OptionPriority priority, Function<OptionDefinition, String> sourceFunction, OptionDefinition implicitDependent, OptionDefinition expandedFrom)438 private ParsedOptionDescription identifyOptionAndPossibleArgument( 439 String arg, 440 Iterator<String> nextArgs, 441 OptionPriority priority, 442 Function<OptionDefinition, String> sourceFunction, 443 OptionDefinition implicitDependent, 444 OptionDefinition expandedFrom) 445 throws OptionsParsingException { 446 447 // Store the way this option was parsed on the command line. 448 StringBuilder commandLineForm = new StringBuilder(); 449 commandLineForm.append(arg); 450 String unconvertedValue = null; 451 OptionDefinition optionDefinition; 452 boolean booleanValue = true; 453 454 if (arg.length() == 2) { // -l (may be nullary or unary) 455 optionDefinition = optionsData.getFieldForAbbrev(arg.charAt(1)); 456 booleanValue = true; 457 458 } else if (arg.length() == 3 && arg.charAt(2) == '-') { // -l- (boolean) 459 optionDefinition = optionsData.getFieldForAbbrev(arg.charAt(1)); 460 booleanValue = false; 461 462 } else if (allowSingleDashLongOptions // -long_option 463 || arg.startsWith("--")) { // or --long_option 464 465 int equalsAt = arg.indexOf('='); 466 int nameStartsAt = arg.startsWith("--") ? 2 : 1; 467 String name = 468 equalsAt == -1 ? arg.substring(nameStartsAt) : arg.substring(nameStartsAt, equalsAt); 469 if (name.trim().isEmpty()) { 470 throw new OptionsParsingException("Invalid options syntax: " + arg, arg); 471 } 472 unconvertedValue = equalsAt == -1 ? null : arg.substring(equalsAt + 1); 473 optionDefinition = optionsData.getOptionDefinitionFromName(name); 474 475 // Look for a "no"-prefixed option name: "no<optionName>". 476 if (optionDefinition == null && name.startsWith("no")) { 477 name = name.substring(2); 478 optionDefinition = optionsData.getOptionDefinitionFromName(name); 479 booleanValue = false; 480 if (optionDefinition != null) { 481 // TODO(bazel-team): Add tests for these cases. 482 if (!optionDefinition.usesBooleanValueSyntax()) { 483 throw new OptionsParsingException( 484 "Illegal use of 'no' prefix on non-boolean option: " + arg, arg); 485 } 486 if (unconvertedValue != null) { 487 throw new OptionsParsingException( 488 "Unexpected value after boolean option: " + arg, arg); 489 } 490 // "no<optionname>" signifies a boolean option w/ false value 491 unconvertedValue = "0"; 492 } 493 } 494 } else { 495 throw new OptionsParsingException("Invalid options syntax: " + arg, arg); 496 } 497 498 if (optionDefinition == null 499 || ImmutableList.copyOf(optionDefinition.getOptionMetadataTags()) 500 .contains(OptionMetadataTag.INTERNAL)) { 501 // Do not recognize internal options, which are treated as if they did not exist. 502 throw new OptionsParsingException("Unrecognized option: " + arg, arg); 503 } 504 505 if (unconvertedValue == null) { 506 // Special-case boolean to supply value based on presence of "no" prefix. 507 if (optionDefinition.usesBooleanValueSyntax()) { 508 unconvertedValue = booleanValue ? "1" : "0"; 509 } else if (optionDefinition.getType().equals(Void.class) 510 && !optionDefinition.isWrapperOption()) { 511 // This is expected, Void type options have no args (unless they're wrapper options). 512 } else if (nextArgs.hasNext()) { 513 // "--flag value" form 514 unconvertedValue = nextArgs.next(); 515 commandLineForm.append(" ").append(unconvertedValue); 516 } else { 517 throw new OptionsParsingException("Expected value after " + arg); 518 } 519 } 520 521 return new ParsedOptionDescription( 522 optionDefinition, 523 commandLineForm.toString(), 524 unconvertedValue, 525 new OptionInstanceOrigin( 526 priority, sourceFunction.apply(optionDefinition), implicitDependent, expandedFrom)); 527 } 528 529 /** 530 * Gets the result of parsing the options. 531 */ getParsedOptions(Class<O> optionsClass)532 <O extends OptionsBase> O getParsedOptions(Class<O> optionsClass) { 533 // Create the instance: 534 O optionsInstance; 535 try { 536 Constructor<O> constructor = optionsData.getConstructor(optionsClass); 537 if (constructor == null) { 538 return null; 539 } 540 optionsInstance = constructor.newInstance(); 541 } catch (ReflectiveOperationException e) { 542 throw new IllegalStateException("Error while instantiating options class", e); 543 } 544 545 // Set the fields 546 for (OptionDefinition optionDefinition : 547 OptionsData.getAllOptionDefinitionsForClass(optionsClass)) { 548 Object value; 549 OptionValueDescription optionValue = optionValues.get(optionDefinition); 550 if (optionValue == null) { 551 value = optionDefinition.getDefaultValue(); 552 } else { 553 value = optionValue.getValue(); 554 } 555 try { 556 optionDefinition.getField().set(optionsInstance, value); 557 } catch (IllegalArgumentException e) { 558 throw new IllegalStateException( 559 String.format("Unable to set %s to value '%s'.", optionDefinition, value), e); 560 } catch (IllegalAccessException e) { 561 throw new IllegalStateException( 562 "Could not set the field due to access issues. This is impossible, as the " 563 + "OptionProcessor checks that all options are non-final public fields.", 564 e); 565 } 566 } 567 return optionsInstance; 568 } 569 getWarnings()570 List<String> getWarnings() { 571 return ImmutableList.copyOf(warnings); 572 } 573 } 574