Home | History | Annotate | Download | only in options
      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. */
     89   OptionsParserImpl(OptionsData optionsData) {
     90     Preconditions.checkNotNull(optionsData);
     91     this.optionsData = optionsData;
     92   }
     93 
     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    */
    102   void setAllowSingleDashLongOptions(boolean allowSingleDashLongOptions) {
    103     this.allowSingleDashLongOptions = allowSingleDashLongOptions;
    104   }
    105 
    106   /** Sets the ArgsPreProcessor for manipulations of the options before parsing. */
    107   void setArgsPreProcessor(ArgsPreProcessor preProcessor) {
    108     this.argsPreProcessor = Preconditions.checkNotNull(preProcessor);
    109   }
    110 
    111   /** Implements {@link OptionsParser#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()}. */
    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}. */
    133   List<String> asCanonicalizedList() {
    134     return asCanonicalizedListOfParsedOptions()
    135         .stream()
    136         .map(ParsedOptionDescription::getDeprecatedCanonicalForm)
    137         .collect(ImmutableList.toImmutableList());
    138   }
    139 
    140   /** Implements {@link OptionsParser#canonicalize}. */
    141   List<ParsedOptionDescription> asCanonicalizedListOfParsedOptions() {
    142     return optionValues
    143         .keySet()
    144         .stream()
    145         .map(optionDefinition -> optionValues.get(optionDefinition).getCanonicalInstances())
    146         .flatMap(Collection::stream)
    147         // Return the effective (canonical) options in the order they were applied.
    148         .sorted(comparing(ParsedOptionDescription::getPriority))
    149         .collect(ImmutableList.toImmutableList());
    150   }
    151 
    152   /** Implements {@link OptionsParser#asListOfOptionValues()}. */
    153   List<OptionValueDescription> asListOfEffectiveOptions() {
    154     List<OptionValueDescription> result = new ArrayList<>();
    155     for (Map.Entry<String, OptionDefinition> mapEntry : optionsData.getAllOptionDefinitions()) {
    156       OptionDefinition optionDefinition = mapEntry.getValue();
    157       OptionValueDescription optionValue = optionValues.get(optionDefinition);
    158       if (optionValue == null) {
    159         result.add(OptionValueDescription.getDefaultOptionValue(optionDefinition));
    160       } else {
    161         result.add(optionValue);
    162       }
    163     }
    164     return result;
    165   }
    166 
    167   private void maybeAddDeprecationWarning(OptionDefinition optionDefinition) {
    168     // Continue to support the old behavior for @Deprecated options.
    169     String warning = optionDefinition.getDeprecationWarning();
    170     if (!warning.isEmpty() || (optionDefinition.getField().isAnnotationPresent(Deprecated.class))) {
    171       addDeprecationWarning(optionDefinition.getOptionName(), warning);
    172     }
    173   }
    174 
    175   private void addDeprecationWarning(String optionName, String warning) {
    176     warnings.add(
    177         String.format(
    178             "Option '%s' is deprecated%s", optionName, (warning.isEmpty() ? "" : ": " + warning)));
    179   }
    180 
    181 
    182   OptionValueDescription clearValue(OptionDefinition optionDefinition)
    183       throws OptionsParsingException {
    184     return optionValues.remove(optionDefinition);
    185   }
    186 
    187   OptionValueDescription getOptionValueDescription(String name) {
    188     OptionDefinition optionDefinition = optionsData.getOptionDefinitionFromName(name);
    189     if (optionDefinition == null) {
    190       throw new IllegalArgumentException("No such option '" + name + "'");
    191     }
    192     return optionValues.get(optionDefinition);
    193   }
    194 
    195   OptionDescription getOptionDescription(String name) throws OptionsParsingException {
    196     OptionDefinition optionDefinition = optionsData.getOptionDefinitionFromName(name);
    197     if (optionDefinition == null) {
    198       return null;
    199     }
    200     return new OptionDescription(optionDefinition, optionsData);
    201   }
    202 
    203   /**
    204    * Implementation of {@link OptionsParser#getExpansionValueDescriptions(OptionDefinition,
    205    * OptionInstanceOrigin)}
    206    */
    207   ImmutableList<ParsedOptionDescription> getExpansionValueDescriptions(
    208       OptionDefinition expansionFlagDef, OptionInstanceOrigin originOfExpansionFlag)
    209       throws OptionsParsingException {
    210     ImmutableList.Builder<ParsedOptionDescription> builder = ImmutableList.builder();
    211 
    212     // Values needed to correctly track the origin of the expanded options.
    213     OptionPriority nextOptionPriority =
    214         OptionPriority.getChildPriority(originOfExpansionFlag.getPriority());
    215     String source;
    216     ParsedOptionDescription implicitDependent = null;
    217     ParsedOptionDescription expandedFrom = null;
    218 
    219     ImmutableList<String> options;
    220     ParsedOptionDescription expansionFlagParsedDummy =
    221         ParsedOptionDescription.newDummyInstance(expansionFlagDef, originOfExpansionFlag);
    222     if (expansionFlagDef.hasImplicitRequirements()) {
    223       options = ImmutableList.copyOf(expansionFlagDef.getImplicitRequirements());
    224       source =
    225           String.format(
    226               "implicitly required by %s (source: %s)",
    227               expansionFlagDef, originOfExpansionFlag.getSource());
    228       implicitDependent = expansionFlagParsedDummy;
    229     } else if (expansionFlagDef.isExpansionOption()) {
    230       options = optionsData.getEvaluatedExpansion(expansionFlagDef);
    231       source =
    232           String.format(
    233               "expanded by %s (source: %s)", expansionFlagDef, originOfExpansionFlag.getSource());
    234       expandedFrom = expansionFlagParsedDummy;
    235     } else {
    236       return ImmutableList.of();
    237     }
    238 
    239     Iterator<String> optionsIterator = options.iterator();
    240     while (optionsIterator.hasNext()) {
    241       String unparsedFlagExpression = optionsIterator.next();
    242       ParsedOptionDescription parsedOption =
    243           identifyOptionAndPossibleArgument(
    244               unparsedFlagExpression,
    245               optionsIterator,
    246               nextOptionPriority,
    247               o -> source,
    248               implicitDependent,
    249               expandedFrom);
    250       builder.add(parsedOption);
    251       nextOptionPriority = OptionPriority.nextOptionPriority(nextOptionPriority);
    252     }
    253     return builder.build();
    254   }
    255 
    256   boolean containsExplicitOption(String name) {
    257     OptionDefinition optionDefinition = optionsData.getOptionDefinitionFromName(name);
    258     if (optionDefinition == null) {
    259       throw new IllegalArgumentException("No such option '" + name + "'");
    260     }
    261     return optionValues.get(optionDefinition) != null;
    262   }
    263 
    264   /**
    265    * Parses the args, and returns what it doesn't parse. May be called multiple times, and may be
    266    * called recursively. The option's definition dictates how it reacts to multiple settings. By
    267    * default, the arg seen last at the highest priority takes precedence, overriding the early
    268    * values. Options that accumulate multiple values will track them in priority and appearance
    269    * order.
    270    */
    271   List<String> parse(
    272       PriorityCategory priorityCat,
    273       Function<OptionDefinition, String> sourceFunction,
    274       List<String> args)
    275       throws OptionsParsingException {
    276     ResidueAndPriority residueAndPriority =
    277         parse(nextPriorityPerPriorityCategory.get(priorityCat), sourceFunction, null, null, args);
    278     nextPriorityPerPriorityCategory.put(priorityCat, residueAndPriority.nextPriority);
    279     return residueAndPriority.residue;
    280   }
    281 
    282   private static final class ResidueAndPriority {
    283     List<String> residue;
    284     OptionPriority nextPriority;
    285 
    286     public ResidueAndPriority(List<String> residue, OptionPriority nextPriority) {
    287       this.residue = residue;
    288       this.nextPriority = nextPriority;
    289     }
    290   }
    291 
    292   /** Implements {@link OptionsParser#parseArgsAsExpansionOfOption} */
    293   List<String> parseArgsAsExpansionOfOption(
    294       ParsedOptionDescription optionToExpand,
    295       Function<OptionDefinition, String> sourceFunction,
    296       List<String> args)
    297       throws OptionsParsingException {
    298     ResidueAndPriority residueAndPriority =
    299         parse(
    300             OptionPriority.getChildPriority(optionToExpand.getPriority()),
    301             sourceFunction,
    302             null,
    303             optionToExpand,
    304             args);
    305     return residueAndPriority.residue;
    306   }
    307 
    308   /**
    309    * Parses the args, and returns what it doesn't parse. May be called multiple times, and may be
    310    * called recursively. Calls may contain intersecting sets of options; in that case, the arg seen
    311    * last takes precedence.
    312    *
    313    * <p>The method treats options that have neither an implicitDependent nor an expandedFrom value
    314    * as explicitly set.
    315    */
    316   private ResidueAndPriority parse(
    317       OptionPriority priority,
    318       Function<OptionDefinition, String> sourceFunction,
    319       ParsedOptionDescription implicitDependent,
    320       ParsedOptionDescription expandedFrom,
    321       List<String> args)
    322       throws OptionsParsingException {
    323     List<String> unparsedArgs = new ArrayList<>();
    324 
    325     Iterator<String> argsIterator = argsPreProcessor.preProcess(args).iterator();
    326     while (argsIterator.hasNext()) {
    327       String arg = argsIterator.next();
    328 
    329       if (!arg.startsWith("-")) {
    330         unparsedArgs.add(arg);
    331         continue;  // not an option arg
    332       }
    333 
    334       if (arg.equals("--")) {  // "--" means all remaining args aren't options
    335         Iterators.addAll(unparsedArgs, argsIterator);
    336         break;
    337       }
    338 
    339       ParsedOptionDescription parsedOption =
    340           identifyOptionAndPossibleArgument(
    341               arg, argsIterator, priority, sourceFunction, implicitDependent, expandedFrom);
    342       handleNewParsedOption(parsedOption);
    343       priority = OptionPriority.nextOptionPriority(priority);
    344     }
    345 
    346     // Go through the final values and make sure they are valid values for their option. Unlike any
    347     // checks that happened above, this also checks that flags that were not set have a valid
    348     // default value. getValue() will throw if the value is invalid.
    349     for (OptionValueDescription valueDescription : asListOfEffectiveOptions()) {
    350       valueDescription.getValue();
    351     }
    352 
    353     return new ResidueAndPriority(unparsedArgs, priority);
    354   }
    355 
    356   /**
    357    * Implementation of {@link OptionsParser#addOptionValueAtSpecificPriority(OptionInstanceOrigin,
    358    * OptionDefinition, String)}
    359    */
    360   void addOptionValueAtSpecificPriority(
    361       OptionInstanceOrigin origin, OptionDefinition option, String unconvertedValue)
    362       throws OptionsParsingException {
    363     Preconditions.checkNotNull(option);
    364     Preconditions.checkNotNull(
    365         unconvertedValue,
    366         "Cannot set %s to a null value. Pass \"\" if an empty value is required.",
    367         option);
    368     Preconditions.checkNotNull(
    369         origin,
    370         "Cannot assign value \'%s\' to %s without a clear origin for this value.",
    371         unconvertedValue,
    372         option);
    373     PriorityCategory priorityCategory = origin.getPriority().getPriorityCategory();
    374     boolean isNotDefault = priorityCategory != OptionPriority.PriorityCategory.DEFAULT;
    375     Preconditions.checkArgument(
    376         isNotDefault,
    377         "Attempt to assign value \'%s\' to %s at priority %s failed. Cannot set options at "
    378             + "default priority - by definition, that means the option is unset.",
    379         unconvertedValue,
    380         option,
    381         priorityCategory);
    382 
    383     handleNewParsedOption(
    384         ParsedOptionDescription.newParsedOptionDescription(
    385             option,
    386             String.format("--%s=%s", option.getOptionName(), unconvertedValue),
    387             unconvertedValue,
    388             origin));
    389   }
    390 
    391   /** Takes care of tracking the parsed option's value in relation to other options. */
    392   private void handleNewParsedOption(ParsedOptionDescription parsedOption)
    393       throws OptionsParsingException {
    394     OptionDefinition optionDefinition = parsedOption.getOptionDefinition();
    395     // All options can be deprecated; check and warn before doing any option-type specific work.
    396     maybeAddDeprecationWarning(optionDefinition);
    397     // Track the value, before any remaining option-type specific work that is done outside of
    398     // the OptionValueDescription.
    399     OptionValueDescription entry =
    400         optionValues.computeIfAbsent(
    401             optionDefinition,
    402             def -> OptionValueDescription.createOptionValueDescription(def, optionsData));
    403     ExpansionBundle expansionBundle = entry.addOptionInstance(parsedOption, warnings);
    404     @Nullable String unconvertedValue = parsedOption.getUnconvertedValue();
    405 
    406     // There are 3 types of flags that expand to other flag values. Expansion flags are the
    407     // accepted way to do this, but implicit requirements also do this. We rely on the
    408     // OptionProcessor compile-time check's guarantee that no option sets
    409     // both expansion behaviors. (In Bazel, --config is another such flag, but that expansion
    410     // is not controlled within the options parser, so we ignore it here)
    411 
    412     // As much as possible, we want the behaviors of these different types of flags to be
    413     // identical, as this minimizes the number of edge cases, but we do not yet track these values
    414     // in the same way.
    415     if (parsedOption.getImplicitDependent() == null) {
    416       // Log explicit options and expanded options in the order they are parsed (can be sorted
    417       // later). This information is needed to correctly canonicalize flags.
    418       parsedOptions.add(parsedOption);
    419     }
    420 
    421     if (expansionBundle != null) {
    422       ResidueAndPriority residueAndPriority =
    423           parse(
    424               OptionPriority.getChildPriority(parsedOption.getPriority()),
    425               o -> expansionBundle.sourceOfExpansionArgs,
    426               optionDefinition.hasImplicitRequirements() ? parsedOption : null,
    427               optionDefinition.isExpansionOption() ? parsedOption : null,
    428               expansionBundle.expansionArgs);
    429       if (!residueAndPriority.residue.isEmpty()) {
    430 
    431           // Throw an assertion here, because this indicates an error in the definition of this
    432           // option's expansion or requirements, not with the input as provided by the user.
    433           throw new AssertionError(
    434               "Unparsed options remain after processing "
    435                   + unconvertedValue
    436                   + ": "
    437                   + Joiner.on(' ').join(residueAndPriority.residue));
    438 
    439       }
    440     }
    441   }
    442 
    443   private ParsedOptionDescription identifyOptionAndPossibleArgument(
    444       String arg,
    445       Iterator<String> nextArgs,
    446       OptionPriority priority,
    447       Function<OptionDefinition, String> sourceFunction,
    448       ParsedOptionDescription implicitDependent,
    449       ParsedOptionDescription expandedFrom)
    450       throws OptionsParsingException {
    451 
    452     // Store the way this option was parsed on the command line.
    453     StringBuilder commandLineForm = new StringBuilder();
    454     commandLineForm.append(arg);
    455     String unconvertedValue = null;
    456     OptionDefinition optionDefinition;
    457     boolean booleanValue = true;
    458 
    459     if (arg.length() == 2) { // -l  (may be nullary or unary)
    460       optionDefinition = optionsData.getFieldForAbbrev(arg.charAt(1));
    461       booleanValue = true;
    462 
    463     } else if (arg.length() == 3 && arg.charAt(2) == '-') { // -l-  (boolean)
    464       optionDefinition = optionsData.getFieldForAbbrev(arg.charAt(1));
    465       booleanValue = false;
    466 
    467     } else if (allowSingleDashLongOptions // -long_option
    468         || arg.startsWith("--")) { // or --long_option
    469 
    470       int equalsAt = arg.indexOf('=');
    471       int nameStartsAt = arg.startsWith("--") ? 2 : 1;
    472       String name =
    473           equalsAt == -1 ? arg.substring(nameStartsAt) : arg.substring(nameStartsAt, equalsAt);
    474       if (name.trim().isEmpty()) {
    475         throw new OptionsParsingException("Invalid options syntax: " + arg, arg);
    476       }
    477       unconvertedValue = equalsAt == -1 ? null : arg.substring(equalsAt + 1);
    478       optionDefinition = optionsData.getOptionDefinitionFromName(name);
    479 
    480       // Look for a "no"-prefixed option name: "no<optionName>".
    481       if (optionDefinition == null && name.startsWith("no")) {
    482         name = name.substring(2);
    483         optionDefinition = optionsData.getOptionDefinitionFromName(name);
    484         booleanValue = false;
    485         if (optionDefinition != null) {
    486           // TODO(bazel-team): Add tests for these cases.
    487           if (!optionDefinition.usesBooleanValueSyntax()) {
    488             throw new OptionsParsingException(
    489                 "Illegal use of 'no' prefix on non-boolean option: " + arg, arg);
    490           }
    491           if (unconvertedValue != null) {
    492             throw new OptionsParsingException(
    493                 "Unexpected value after boolean option: " + arg, arg);
    494           }
    495           // "no<optionname>" signifies a boolean option w/ false value
    496           unconvertedValue = "0";
    497         }
    498       }
    499     } else {
    500       throw new OptionsParsingException("Invalid options syntax: " + arg, arg);
    501     }
    502 
    503     if (optionDefinition == null
    504         || ImmutableList.copyOf(optionDefinition.getOptionMetadataTags())
    505             .contains(OptionMetadataTag.INTERNAL)) {
    506       // Do not recognize internal options, which are treated as if they did not exist.
    507       throw new OptionsParsingException("Unrecognized option: " + arg, arg);
    508     }
    509 
    510     if (unconvertedValue == null) {
    511       // Special-case boolean to supply value based on presence of "no" prefix.
    512       if (optionDefinition.usesBooleanValueSyntax()) {
    513         unconvertedValue = booleanValue ? "1" : "0";
    514       } else if (optionDefinition.getType().equals(Void.class)) {
    515         // This is expected, Void type options have no args.
    516       } else if (nextArgs.hasNext()) {
    517         // "--flag value" form
    518         unconvertedValue = nextArgs.next();
    519         commandLineForm.append(" ").append(unconvertedValue);
    520       } else {
    521         throw new OptionsParsingException("Expected value after " + arg);
    522       }
    523     }
    524 
    525     return ParsedOptionDescription.newParsedOptionDescription(
    526         optionDefinition,
    527         commandLineForm.toString(),
    528         unconvertedValue,
    529         new OptionInstanceOrigin(
    530             priority, sourceFunction.apply(optionDefinition), implicitDependent, expandedFrom));
    531   }
    532 
    533   /**
    534    * Gets the result of parsing the options.
    535    */
    536   <O extends OptionsBase> O getParsedOptions(Class<O> optionsClass) {
    537     // Create the instance:
    538     O optionsInstance;
    539     try {
    540       Constructor<O> constructor = optionsData.getConstructor(optionsClass);
    541       if (constructor == null) {
    542         return null;
    543       }
    544       optionsInstance = constructor.newInstance();
    545     } catch (ReflectiveOperationException e) {
    546       throw new IllegalStateException("Error while instantiating options class", e);
    547     }
    548 
    549     // Set the fields
    550     for (OptionDefinition optionDefinition :
    551         OptionsData.getAllOptionDefinitionsForClass(optionsClass)) {
    552       Object value;
    553       OptionValueDescription optionValue = optionValues.get(optionDefinition);
    554       if (optionValue == null) {
    555         value = optionDefinition.getDefaultValue();
    556       } else {
    557         value = optionValue.getValue();
    558       }
    559       try {
    560         optionDefinition.getField().set(optionsInstance, value);
    561       } catch (IllegalArgumentException e) {
    562         throw new IllegalStateException(
    563             String.format("Unable to set %s to value '%s'.", optionDefinition, value), e);
    564       } catch (IllegalAccessException e) {
    565         throw new IllegalStateException(
    566             "Could not set the field due to access issues. This is impossible, as the "
    567                 + "OptionProcessor checks that all options are non-final public fields.",
    568             e);
    569       }
    570     }
    571     return optionsInstance;
    572   }
    573 
    574   List<String> getWarnings() {
    575     return ImmutableList.copyOf(warnings);
    576   }
    577 }
    578