aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/common/options/OptionsParser.java
blob: 6f1d7b647954c51020914333441760614d5f48f9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
// Copyright 2014 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.devtools.common.options;

import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ListMultimap;
import com.google.common.escape.Escaper;
import com.google.devtools.common.options.OptionDefinition.NotAnOptionException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * A parser for options. Typical use case in a main method:
 *
 * <pre>
 * OptionsParser parser = OptionsParser.newOptionsParser(FooOptions.class, BarOptions.class);
 * parser.parseAndExitUponError(args);
 * FooOptions foo = parser.getOptions(FooOptions.class);
 * BarOptions bar = parser.getOptions(BarOptions.class);
 * List&lt;String&gt; otherArguments = parser.getResidue();
 * </pre>
 *
 * <p>FooOptions and BarOptions would be options specification classes, derived from OptionsBase,
 * that contain fields annotated with @Option(...).
 *
 * <p>Alternatively, rather than calling {@link
 * #parseAndExitUponError(OptionPriority.PriorityCategory, String, String[])}, client code may call
 * {@link #parse(OptionPriority.PriorityCategory,String,List)}, and handle parser exceptions usage
 * messages themselves.
 *
 * <p>This options parsing implementation has (at least) one design flaw. It allows both '--foo=baz'
 * and '--foo baz' for all options except void, boolean and tristate options. For these, the 'baz'
 * in '--foo baz' is not treated as a parameter to the option, making it is impossible to switch
 * options between void/boolean/tristate and everything else without breaking backwards
 * compatibility.
 *
 * @see Options a simpler class which you can use if you only have one options specification class
 */
public class OptionsParser implements OptionsProvider {

  // TODO(b/65049598) make ConstructionException checked.
  /**
   * An unchecked exception thrown when there is a problem constructing a parser, e.g. an error
   * while validating an {@link OptionDefinition} in one of its {@link OptionsBase} subclasses.
   *
   * <p>This exception is unchecked because it generally indicates an internal error affecting all
   * invocations of the program. I.e., any such error should be immediately obvious to the
   * developer. Although unchecked, we explicitly mark some methods as throwing it as a reminder in
   * the API.
   */
  public static class ConstructionException extends RuntimeException {
    public ConstructionException(String message) {
      super(message);
    }

    public ConstructionException(Throwable cause) {
      super(cause);
    }

    public ConstructionException(String message, Throwable cause) {
      super(message, cause);
    }
  }

  /**
   * A cache for the parsed options data. Both keys and values are immutable, so
   * this is always safe. Only access this field through the {@link
   * #getOptionsData} method for thread-safety! The cache is very unlikely to
   * grow to a significant amount of memory, because there's only a fixed set of
   * options classes on the classpath.
   */
  private static final Map<ImmutableList<Class<? extends OptionsBase>>, OptionsData> optionsData =
      new HashMap<>();

  /**
   * Returns {@link OpaqueOptionsData} suitable for passing along to {@link
   * #newOptionsParser(OpaqueOptionsData optionsData)}.
   *
   * <p>This is useful when you want to do the work of analyzing the given {@code optionsClasses}
   * exactly once, but you want to parse lots of different lists of strings (and thus need to
   * construct lots of different {@link OptionsParser} instances).
   */
  public static OpaqueOptionsData getOptionsData(
      List<Class<? extends OptionsBase>> optionsClasses) throws ConstructionException {
    return getOptionsDataInternal(optionsClasses);
  }

  /**
   * Returns the {@link OptionsData} associated with the given list of options classes.
   */
  static synchronized OptionsData getOptionsDataInternal(
      List<Class<? extends OptionsBase>> optionsClasses) throws ConstructionException {
    ImmutableList<Class<? extends OptionsBase>> immutableOptionsClasses =
        ImmutableList.copyOf(optionsClasses);
    OptionsData result = optionsData.get(immutableOptionsClasses);
    if (result == null) {
      try {
        result = OptionsData.from(immutableOptionsClasses);
      } catch (Exception e) {
        Throwables.throwIfInstanceOf(e, ConstructionException.class);
        throw new ConstructionException(e.getMessage(), e);
      }
      optionsData.put(immutableOptionsClasses, result);
    }
    return result;
  }

  /**
   * Returns the {@link OptionsData} associated with the given options class.
   */
  static OptionsData getOptionsDataInternal(Class<? extends OptionsBase> optionsClass)
      throws ConstructionException {
    return getOptionsDataInternal(ImmutableList.of(optionsClass));
  }

  /**
   * @see #newOptionsParser(Iterable)
   */
  public static OptionsParser newOptionsParser(Class<? extends OptionsBase> class1)
      throws ConstructionException {
    return newOptionsParser(ImmutableList.<Class<? extends OptionsBase>>of(class1));
  }

  /** @see #newOptionsParser(Iterable) */
  public static OptionsParser newOptionsParser(
      Class<? extends OptionsBase> class1, Class<? extends OptionsBase> class2)
      throws ConstructionException {
    return newOptionsParser(ImmutableList.of(class1, class2));
  }

  /** Create a new {@link OptionsParser}. */
  public static OptionsParser newOptionsParser(
      Iterable<? extends Class<? extends OptionsBase>> optionsClasses)
      throws ConstructionException {
    return newOptionsParser(getOptionsDataInternal(ImmutableList.copyOf(optionsClasses)));
  }

  /**
   * Create a new {@link OptionsParser}, using {@link OpaqueOptionsData} previously returned from
   * {@link #getOptionsData}.
   */
  public static OptionsParser newOptionsParser(OpaqueOptionsData optionsData) {
    return new OptionsParser((OptionsData) optionsData);
  }

  private final OptionsParserImpl impl;
  private final List<String> residue = new ArrayList<String>();
  private boolean allowResidue = true;

  OptionsParser(OptionsData optionsData) {
    impl = new OptionsParserImpl(optionsData);
  }

  /**
   * Indicates whether or not the parser will allow a non-empty residue; that
   * is, iff this value is true then a call to one of the {@code parse}
   * methods will throw {@link OptionsParsingException} unless
   * {@link #getResidue()} is empty after parsing.
   */
  public void setAllowResidue(boolean allowResidue) {
    this.allowResidue = allowResidue;
  }

  /**
   * Indicates whether or not the parser will allow long options with a
   * single-dash, instead of the usual double-dash, too, eg. -example instead of just --example.
   */
  public void setAllowSingleDashLongOptions(boolean allowSingleDashLongOptions) {
    this.impl.setAllowSingleDashLongOptions(allowSingleDashLongOptions);
  }

  /**
   * Enables the Parser to handle params files using the provided {@link ParamsFilePreProcessor}.
   */
  public void enableParamsFileSupport(ParamsFilePreProcessor preProcessor) {
    this.impl.setArgsPreProcessor(preProcessor);
  }

  public void parseAndExitUponError(String[] args) {
    parseAndExitUponError(OptionPriority.PriorityCategory.COMMAND_LINE, "unknown", args);
  }

  /**
   * A convenience function for use in main methods. Parses the command line parameters, and exits
   * upon error. Also, prints out the usage message if "--help" appears anywhere within {@code
   * args}.
   */
  public void parseAndExitUponError(
      OptionPriority.PriorityCategory priority, String source, String[] args) {
    for (String arg : args) {
      if (arg.equals("--help")) {
        System.out.println(
            describeOptionsWithDeprecatedCategories(ImmutableMap.of(), HelpVerbosity.LONG));

        System.exit(0);
      }
    }
    try {
      parse(priority, source, Arrays.asList(args));
    } catch (OptionsParsingException e) {
      System.err.println("Error parsing command line: " + e.getMessage());
      System.err.println("Try --help.");
      System.exit(2);
    }
  }

  /** The metadata about an option, in the context of this options parser. */
  public static final class OptionDescription {
    private final OptionDefinition optionDefinition;
    private final ImmutableList<String> evaluatedExpansion;

    OptionDescription(OptionDefinition definition, OptionsData optionsData) {
      this.optionDefinition = definition;
      this.evaluatedExpansion = optionsData.getEvaluatedExpansion(optionDefinition);
    }

    public OptionDefinition getOptionDefinition() {
      return optionDefinition;
    }

    public boolean isExpansion() {
      return optionDefinition.isExpansionOption();
    }

    /** Return a list of flags that this option expands to. */
    public ImmutableList<String> getExpansion() throws OptionsParsingException {
      return evaluatedExpansion;
    }

    @Override
    public boolean equals(Object obj) {
      if (obj instanceof OptionDescription) {
        OptionDescription other = (OptionDescription) obj;
        // Check that the option is the same, with the same expansion.
        return other.optionDefinition.equals(optionDefinition)
            && other.evaluatedExpansion.equals(evaluatedExpansion);
      }
      return false;
    }

    @Override
    public int hashCode() {
      return optionDefinition.hashCode() + evaluatedExpansion.hashCode();
    }
  }

  /**
   * The verbosity with which option help messages are displayed: short (just
   * the name), medium (name, type, default, abbreviation), and long (full
   * description).
   */
  public enum HelpVerbosity { LONG, MEDIUM, SHORT }

  /**
   * Returns a description of all the options this parser can digest. In addition to {@link Option}
   * annotations, this method also interprets {@link OptionsUsage} annotations which give an
   * intuitive short description for the options. Options of the same category (see {@link
   * OptionDocumentationCategory}) will be grouped together.
   *
   * @param productName the name of this product (blaze, bazel)
   * @param helpVerbosity if {@code long}, the options will be described verbosely, including their
   *     types, defaults and descriptions. If {@code medium}, the descriptions are omitted, and if
   *     {@code short}, the options are just enumerated.
   */
  public String describeOptions(String productName, HelpVerbosity helpVerbosity) {
    StringBuilder desc = new StringBuilder();
    LinkedHashMap<OptionDocumentationCategory, List<OptionDefinition>> optionsByCategory =
        getOptionsSortedByCategory();
    ImmutableMap<OptionDocumentationCategory, String> optionCategoryDescriptions =
        OptionFilterDescriptions.getOptionCategoriesEnumDescription(productName);
    for (Map.Entry<OptionDocumentationCategory, List<OptionDefinition>> e :
        optionsByCategory.entrySet()) {
      String categoryDescription = optionCategoryDescriptions.get(e.getKey());
      List<OptionDefinition> categorizedOptionList = e.getValue();

      // Describe the category if we're going to end up using it at all.
      if (!categorizedOptionList.isEmpty()) {
        desc.append("\n").append(categoryDescription).append(":\n");
      }
      // Describe the options in this category.
      for (OptionDefinition optionDef : categorizedOptionList) {
        OptionsUsage.getUsage(optionDef, desc, helpVerbosity, impl.getOptionsData(), true);
      }
    }

    return desc.toString().trim();
  }

  /**
   * @return all documented options loaded in this parser, grouped by categories in display order.
   */
  private LinkedHashMap<OptionDocumentationCategory, List<OptionDefinition>>
      getOptionsSortedByCategory() {
    OptionsData data = impl.getOptionsData();
    if (data.getOptionsClasses().isEmpty()) {
      return new LinkedHashMap<>();
    }

    // Get the documented options grouped by category.
    ListMultimap<OptionDocumentationCategory, OptionDefinition> optionsByCategories =
        ArrayListMultimap.create();
    for (Class<? extends OptionsBase> optionsClass : data.getOptionsClasses()) {
      for (OptionDefinition optionDefinition :
          OptionsData.getAllOptionDefinitionsForClass(optionsClass)) {
        // Only track documented options.
        if (optionDefinition.getDocumentationCategory()
            != OptionDocumentationCategory.UNDOCUMENTED) {
          optionsByCategories.put(optionDefinition.getDocumentationCategory(), optionDefinition);
        }
      }
    }

    // Put the categories into display order and sort the options in each category.
    LinkedHashMap<OptionDocumentationCategory, List<OptionDefinition>> sortedCategoriesToOptions =
        new LinkedHashMap<>(OptionFilterDescriptions.documentationOrder.length, 1);
    for (OptionDocumentationCategory category : OptionFilterDescriptions.documentationOrder) {
      List<OptionDefinition> optionList = optionsByCategories.get(category);
      if (optionList != null) {
        optionList.sort(OptionDefinition.BY_OPTION_NAME);
        sortedCategoriesToOptions.put(category, optionList);
      }
    }
    return sortedCategoriesToOptions;
  }

  /**
   * Returns a description of all the options this parser can digest. In addition to {@link Option}
   * annotations, this method also interprets {@link OptionsUsage} annotations which give an
   * intuitive short description for the options. Options of the same category (see {@link
   * Option#category}) will be grouped together.
   *
   * @param categoryDescriptions a mapping from category names to category descriptions.
   *     Descriptions are optional; if omitted, a string based on the category name will be used.
   * @param helpVerbosity if {@code long}, the options will be described verbosely, including their
   *     types, defaults and descriptions. If {@code medium}, the descriptions are omitted, and if
   *     {@code short}, the options are just enumerated.
   */
  @Deprecated
  public String describeOptionsWithDeprecatedCategories(
      Map<String, String> categoryDescriptions, HelpVerbosity helpVerbosity) {
    OptionsData data = impl.getOptionsData();
    StringBuilder desc = new StringBuilder();
    if (!data.getOptionsClasses().isEmpty()) {
      List<OptionDefinition> allFields = new ArrayList<>();
      for (Class<? extends OptionsBase> optionsClass : data.getOptionsClasses()) {
        allFields.addAll(OptionsData.getAllOptionDefinitionsForClass(optionsClass));
      }
      Collections.sort(allFields, OptionDefinition.BY_CATEGORY);
      String prevCategory = null;

      for (OptionDefinition optionDefinition : allFields) {
        String category = optionDefinition.getOptionCategory();
        if (!category.equals(prevCategory)
            && optionDefinition.getDocumentationCategory()
                != OptionDocumentationCategory.UNDOCUMENTED) {
          String description = categoryDescriptions.get(category);
          if (description == null) {
            description = "Options category '" + category + "'";
          }
          desc.append("\n").append(description).append(":\n");
          prevCategory = category;
        }

        if (optionDefinition.getDocumentationCategory()
            != OptionDocumentationCategory.UNDOCUMENTED) {
          OptionsUsage.getUsage(
              optionDefinition, desc, helpVerbosity, impl.getOptionsData(), false);
        }
      }
    }
    return desc.toString().trim();
  }

  /**
   * Returns a description of all the options this parser can digest. In addition to {@link Option}
   * annotations, this method also interprets {@link OptionsUsage} annotations which give an
   * intuitive short description for the options.
   *
   * @param categoryDescriptions a mapping from category names to category descriptions. Options of
   *     the same category (see {@link Option#category}) will be grouped together, preceded by the
   *     description of the category.
   */
  @Deprecated
  public String describeOptionsHtmlWithDeprecatedCategories(
      Map<String, String> categoryDescriptions, Escaper escaper) {
    OptionsData data = impl.getOptionsData();
    StringBuilder desc = new StringBuilder();
    if (!data.getOptionsClasses().isEmpty()) {
      List<OptionDefinition> allFields = new ArrayList<>();
      for (Class<? extends OptionsBase> optionsClass : data.getOptionsClasses()) {
        allFields.addAll(OptionsData.getAllOptionDefinitionsForClass(optionsClass));
      }
      Collections.sort(allFields, OptionDefinition.BY_CATEGORY);
      String prevCategory = null;

      for (OptionDefinition optionDefinition : allFields) {
        String category = optionDefinition.getOptionCategory();
        if (!category.equals(prevCategory)
            && optionDefinition.getDocumentationCategory()
                != OptionDocumentationCategory.UNDOCUMENTED) {
          String description = categoryDescriptions.get(category);
          if (description == null) {
            description = "Options category '" + category + "'";
          }
          if (prevCategory != null) {
            desc.append("</dl>\n\n");
          }
          desc.append(escaper.escape(description)).append(":\n");
          desc.append("<dl>");
          prevCategory = category;
        }

        if (optionDefinition.getDocumentationCategory()
            != OptionDocumentationCategory.UNDOCUMENTED) {
          OptionsUsage.getUsageHtml(optionDefinition, desc, escaper, impl.getOptionsData(), false);
        }
      }
      desc.append("</dl>\n");
    }
    return desc.toString();
  }

  /**
   * Returns a description of all the options this parser can digest. In addition to {@link Option}
   * annotations, this method also interprets {@link OptionsUsage} annotations which give an
   * intuitive short description for the options.
   */
  public String describeOptionsHtml(Escaper escaper, String productName) {
    StringBuilder desc = new StringBuilder();
    LinkedHashMap<OptionDocumentationCategory, List<OptionDefinition>> optionsByCategory =
        getOptionsSortedByCategory();
    ImmutableMap<OptionDocumentationCategory, String> optionCategoryDescriptions =
        OptionFilterDescriptions.getOptionCategoriesEnumDescription(productName);

    for (Map.Entry<OptionDocumentationCategory, List<OptionDefinition>> e :
        optionsByCategory.entrySet()) {
      desc.append("<dl>");
      String categoryDescription = optionCategoryDescriptions.get(e.getKey());
      List<OptionDefinition> categorizedOptionsList = e.getValue();

      // Describe the category if we're going to end up using it at all.
      if (!categorizedOptionsList.isEmpty()) {
        desc.append(escaper.escape(categoryDescription)).append(":\n");
      }
      // Describe the options in this category.
      for (OptionDefinition optionDef : categorizedOptionsList) {
        OptionsUsage.getUsageHtml(optionDef, desc, escaper, impl.getOptionsData(), true);
      }
      desc.append("</dl>\n");
    }
    return desc.toString();
  }

  /**
   * Returns a string listing the possible flag completion for this command along with the command
   * completion if any. See {@link OptionsUsage#getCompletion(OptionDefinition, StringBuilder)} for
   * more details on the format for the flag completion.
   */
  public String getOptionsCompletion() {
    StringBuilder desc = new StringBuilder();

    visitOptions(
        optionDefinition ->
            optionDefinition.getDocumentationCategory() != OptionDocumentationCategory.UNDOCUMENTED,
        optionDefinition -> OptionsUsage.getCompletion(optionDefinition, desc));

    return desc.toString();
  }

  public void visitOptions(
      Predicate<OptionDefinition> predicate, Consumer<OptionDefinition> visitor) {
    Preconditions.checkNotNull(predicate, "Missing predicate.");
    Preconditions.checkNotNull(visitor, "Missing visitor.");

    OptionsData data = impl.getOptionsData();
    data.getOptionsClasses()
        // List all options
        .stream()
        .flatMap(optionsClass -> OptionsData.getAllOptionDefinitionsForClass(optionsClass).stream())
        // Sort field for deterministic ordering
        .sorted(OptionDefinition.BY_OPTION_NAME)
        .filter(predicate)
        .forEach(visitor);
  }

  /**
   * Returns a description of the option.
   *
   * @return The {@link OptionDescription} for the option, or null if there is no option by the
   *     given name.
   */
  OptionDescription getOptionDescription(String name) throws OptionsParsingException {
    return impl.getOptionDescription(name);
  }

  /**
   * Returns the parsed options that get expanded from this option, whether it expands due to an
   * implicit requirement or expansion.
   *
   * @param expansionOption the option that might need to be expanded. If this option does not
   *     expand to other options, the empty list will be returned.
   * @param originOfExpansionOption the origin of the option that's being expanded. This function
   *     will take care of adjusting the source messages as necessary.
   */
  ImmutableList<ParsedOptionDescription> getExpansionValueDescriptions(
      OptionDefinition expansionOption, OptionInstanceOrigin originOfExpansionOption)
      throws OptionsParsingException {
    return impl.getExpansionValueDescriptions(expansionOption, originOfExpansionOption);
  }

  /**
   * Returns a description of the option value set by the last previous call to {@link
   * #parse(OptionPriority.PriorityCategory, String, List)} that successfully set the given option.
   * If the option is of type {@link List}, the description will correspond to any one of the calls,
   * but not necessarily the last.
   *
   * @return The {@link com.google.devtools.common.options.OptionValueDescription} for the option,
   *     or null if the value has not been set.
   * @throws IllegalArgumentException if there is no option by the given name.
   */
  public OptionValueDescription getOptionValueDescription(String name) {
    return impl.getOptionValueDescription(name);
  }

  /**
   * A convenience method, equivalent to {@code parse(PriorityCategory.COMMAND_LINE, null,
   * Arrays.asList(args))}.
   */
  public void parse(String... args) throws OptionsParsingException {
    parse(OptionPriority.PriorityCategory.COMMAND_LINE, null, Arrays.asList(args));
  }

  /**
   * A convenience method, equivalent to {@code parse(PriorityCategory.COMMAND_LINE, null, args)}.
   */
  public void parse(List<String> args) throws OptionsParsingException {
    parse(OptionPriority.PriorityCategory.COMMAND_LINE, null, args);
  }

  /**
   * Parses {@code args}, using the classes registered with this parser, at the given priority.
   *
   * <p>May be called multiple times; later options override existing ones if they have equal or
   * higher priority. Strings that cannot be parsed as options are accumulated as residue, if this
   * parser allows it.
   *
   * <p>{@link #getOptions(Class)} and {@link #getResidue()} will return the results.
   *
   * @param priority the priority at which to parse these options. Within this priority category,
   *     each option will be given an index to track its position. If parse() has already been
   *     called at this priority, the indexing will continue where it left off, to keep ordering.
   * @param source the source to track for each option parsed.
   * @param args the arg list to parse. Each element might be an option, a value linked to an
   *     option, or residue.
   */
  public void parse(OptionPriority.PriorityCategory priority, String source, List<String> args)
      throws OptionsParsingException {
    parseWithSourceFunction(priority, o -> source, args);
  }

  /**
   * Parses {@code args}, using the classes registered with this parser, at the given priority.
   *
   * <p>May be called multiple times; later options override existing ones if they have equal or
   * higher priority. Strings that cannot be parsed as options are accumulated as residue, if this
   * parser allows it.
   *
   * <p>{@link #getOptions(Class)} and {@link #getResidue()} will return the results.
   *
   * @param priority the priority at which to parse these options. Within this priority category,
   *     each option will be given an index to track its position. If parse() has already been
   *     called at this priority, the indexing will continue where it left off, to keep ordering.
   * @param sourceFunction a function that maps option names to the source of the option.
   * @param args the arg list to parse. Each element might be an option, a value linked to an
   *     option, or residue.
   */
  public void parseWithSourceFunction(
      OptionPriority.PriorityCategory priority,
      Function<OptionDefinition, String> sourceFunction,
      List<String> args)
      throws OptionsParsingException {
    Preconditions.checkNotNull(priority);
    Preconditions.checkArgument(priority != OptionPriority.PriorityCategory.DEFAULT);
    residue.addAll(impl.parse(priority, sourceFunction, args));
    if (!allowResidue && !residue.isEmpty()) {
      String errorMsg = "Unrecognized arguments: " + Joiner.on(' ').join(residue);
      throw new OptionsParsingException(errorMsg);
    }
  }

  /**
   * Parses the args at the priority of the provided option. This is useful for after-the-fact
   * expansion.
   *
   * @param optionToExpand the option that is being "expanded" after the fact. The provided args
   *     will have the same priority as this option.
   * @param source a description of where the expansion arguments came from.
   * @param args the arguments to parse as the expansion. Order matters, as the value of a flag may
   *     be in the following argument.
   */
  public void parseArgsAsExpansionOfOption(
      ParsedOptionDescription optionToExpand, String source, List<String> args)
      throws OptionsParsingException {
    Preconditions.checkNotNull(
        optionToExpand, "Option for expansion not specified for arglist " + args);
    Preconditions.checkArgument(
        optionToExpand.getPriority().getPriorityCategory()
            != OptionPriority.PriorityCategory.DEFAULT,
        "Priority cannot be default, which was specified for arglist " + args);
    residue.addAll(impl.parseArgsAsExpansionOfOption(optionToExpand, o -> source, args));
    if (!allowResidue && !residue.isEmpty()) {
      String errorMsg = "Unrecognized arguments: " + Joiner.on(' ').join(residue);
      throw new OptionsParsingException(errorMsg);
    }
  }

  /**
   * @param origin the origin of this option instance, it includes the priority of the value. If
   *     other values have already been or will be parsed at a higher priority, they might override
   *     the provided value. If this option already has a value at this priority, this value will
   *     have precedence, but this should be avoided, as it breaks order tracking.
   * @param option the option to add the value for.
   * @param value the value to add at the given priority.
   */
  void addOptionValueAtSpecificPriority(
      OptionInstanceOrigin origin, OptionDefinition option, String value)
      throws OptionsParsingException {
    impl.addOptionValueAtSpecificPriority(origin, option, value);
  }

  /**
   * Clears the given option.
   *
   * <p>This will not affect options objects that have already been retrieved from this parser
   * through {@link #getOptions(Class)}.
   *
   * @param option The option to clear.
   * @return The old value of the option that was cleared.
   * @throws IllegalArgumentException If the flag does not exist.
   */
  public OptionValueDescription clearValue(OptionDefinition option) throws OptionsParsingException {
    return impl.clearValue(option);
  }

  @Override
  public List<String> getResidue() {
    return ImmutableList.copyOf(residue);
  }

  /** Returns a list of warnings about problems encountered by previous parse calls. */
  public List<String> getWarnings() {
    return impl.getWarnings();
  }

  @Override
  public <O extends OptionsBase> O getOptions(Class<O> optionsClass) {
    return impl.getParsedOptions(optionsClass);
  }

  @Override
  public boolean containsExplicitOption(String name) {
    return impl.containsExplicitOption(name);
  }

  @Override
  public List<ParsedOptionDescription> asCompleteListOfParsedOptions() {
    return impl.asCompleteListOfParsedOptions();
  }

  @Override
  public List<ParsedOptionDescription> asListOfExplicitOptions() {
    return impl.asListOfExplicitOptions();
  }

  @Override
  public List<ParsedOptionDescription> asListOfCanonicalOptions() {
    return impl.asCanonicalizedListOfParsedOptions();
  }

  @Override
  public List<OptionValueDescription> asListOfOptionValues() {
    return impl.asListOfEffectiveOptions();
  }

  @Override
  public List<String> canonicalize() {
    return impl.asCanonicalizedList();
  }

  /** Returns all options fields of the given options class, in alphabetic order. */
  public static ImmutableList<OptionDefinition> getOptionDefinitions(
      Class<? extends OptionsBase> optionsClass) {
    return OptionsData.getAllOptionDefinitionsForClass(optionsClass);
  }

  /**
   * Returns whether the given options class uses only the core types listed in {@link
   * UsesOnlyCoreTypes#CORE_TYPES}. These are guaranteed to be deeply immutable and serializable.
   */
  public static boolean getUsesOnlyCoreTypes(Class<? extends OptionsBase> optionsClass) {
    OptionsData data = OptionsParser.getOptionsDataInternal(optionsClass);
    return data.getUsesOnlyCoreTypes(optionsClass);
  }

  /**
   * Returns a mapping from each option {@link Field} in {@code optionsClass} (including inherited
   * ones) to its value in {@code options}.
   *
   * <p>To save space, the map directly stores {@code Fields} instead of the {@code
   * OptionDefinitions}.
   *
   * <p>The map is a mutable copy; changing the map won't affect {@code options} and vice versa. The
   * map entries appear sorted alphabetically by option name.
   *
   * <p>If {@code options} is an instance of a subclass of {@link OptionsBase}, any options defined
   * by the subclass are not included in the map, only the options declared in the provided class
   * are included.
   *
   * @throws IllegalArgumentException if {@code options} is not an instance of {@link OptionsBase}
   */
  public static <O extends OptionsBase> Map<Field, Object> toMap(Class<O> optionsClass, O options) {
    // Alphabetized due to getAllOptionDefinitionsForClass()'s order.
    Map<Field, Object> map = new LinkedHashMap<>();
    for (OptionDefinition optionDefinition :
        OptionsData.getAllOptionDefinitionsForClass(optionsClass)) {
      try {
        // Get the object value of the optionDefinition and place in map.
        map.put(optionDefinition.getField(), optionDefinition.getField().get(options));
      } catch (IllegalAccessException e) {
        // All options fields of options classes should be public.
        throw new IllegalStateException(e);
      } catch (IllegalArgumentException e) {
        // This would indicate an inconsistency in the cached OptionsData.
        throw new IllegalStateException(e);
      }
    }
    return map;
  }

  /**
   * Given a mapping as returned by {@link #toMap}, and the options class it that its entries
   * correspond to, this constructs the corresponding instance of the options class.
   *
   * @param map Field to Object, expecting an entry for each field in the optionsClass. This
   *     directly refers to the Field, without wrapping it in an OptionDefinition, see {@link
   *     #toMap}.
   * @throws IllegalArgumentException if {@code map} does not contain exactly the fields of {@code
   *     optionsClass}, with values of the appropriate type
   */
  public static <O extends OptionsBase> O fromMap(Class<O> optionsClass, Map<Field, Object> map) {
    // Instantiate the options class.
    OptionsData data = getOptionsDataInternal(optionsClass);
    O optionsInstance;
    try {
      Constructor<O> constructor = data.getConstructor(optionsClass);
      Preconditions.checkNotNull(constructor, "No options class constructor available");
      optionsInstance = constructor.newInstance();
    } catch (ReflectiveOperationException e) {
      throw new IllegalStateException("Error while instantiating options class", e);
    }

    List<OptionDefinition> optionDefinitions =
        OptionsData.getAllOptionDefinitionsForClass(optionsClass);
    // Ensure all fields are covered, no extraneous fields.
    validateFieldsSets(optionsClass, new LinkedHashSet<Field>(map.keySet()));
    // Populate the instance.
    for (OptionDefinition optionDefinition : optionDefinitions) {
      // Non-null as per above check.
      Object value = map.get(optionDefinition.getField());
      try {
        optionDefinition.getField().set(optionsInstance, value);
      } catch (IllegalAccessException e) {
        throw new IllegalStateException(e);
      }
      // May also throw IllegalArgumentException if map value is ill typed.
    }
    return optionsInstance;
  }

  /**
   * Raises a pretty {@link IllegalArgumentException} if the provided set of fields is a complete
   * set for the optionsClass.
   *
   * <p>The entries in {@code fieldsFromMap} may be ill formed by being null or lacking an {@link
   * Option} annotation.
   */
  private static void validateFieldsSets(
      Class<? extends OptionsBase> optionsClass, LinkedHashSet<Field> fieldsFromMap) {
    ImmutableList<OptionDefinition> optionDefsFromClasses =
        OptionsData.getAllOptionDefinitionsForClass(optionsClass);
    Set<Field> fieldsFromClass =
        optionDefsFromClasses.stream().map(OptionDefinition::getField).collect(Collectors.toSet());

    if (fieldsFromClass.equals(fieldsFromMap)) {
      // They are already equal, avoid additional checks.
      return;
    }

    List<String> extraNamesFromClass = new ArrayList<>();
    List<String> extraNamesFromMap = new ArrayList<>();
    for (OptionDefinition optionDefinition : optionDefsFromClasses) {
      if (!fieldsFromMap.contains(optionDefinition.getField())) {
        extraNamesFromClass.add("'" + optionDefinition.getOptionName() + "'");
      }
    }
    for (Field field : fieldsFromMap) {
      // Extra validation on the map keys since they don't come from OptionsData.
      if (!fieldsFromClass.contains(field)) {
        if (field == null) {
          extraNamesFromMap.add("<null field>");
        } else {
          OptionDefinition optionDefinition = null;
          try {
            // TODO(ccalvarin) This shouldn't be necessary, no option definitions should be found in
            // this optionsClass that weren't in the cache.
            optionDefinition = OptionDefinition.extractOptionDefinition(field);
            extraNamesFromMap.add("'" + optionDefinition.getOptionName() + "'");
          } catch (NotAnOptionException e) {
            extraNamesFromMap.add("<non-Option field>");
          }
        }
      }
    }
    throw new IllegalArgumentException(
        "Map keys do not match fields of options class; extra map keys: {"
            + Joiner.on(", ").join(extraNamesFromMap)
            + "}; extra options class options: {"
            + Joiner.on(", ").join(extraNamesFromClass)
            + "}");
  }
}