Add prefix options to CliParser.

This is required to support

'--map-diagnostics[:<type>] <from> <to>'

Bug: b/510714591
Change-Id: I98eb87f2a7074ee8e7e430d31c1a9a9496bf4c8d
diff --git a/src/main/java/com/android/tools/r8/utils/CliParserUtils.java b/src/main/java/com/android/tools/r8/utils/CliParserUtils.java
index 60a8b79..33bccc7 100644
--- a/src/main/java/com/android/tools/r8/utils/CliParserUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/CliParserUtils.java
@@ -27,13 +27,19 @@
     List<ParseFlagInfo> flags = new ArrayList<>();
     for (OptionInfo info : parser.getOptionInfo()) {
       List<String> helpLines = StringUtils.wrapToWidth(info.description, descriptionWidth);
-      List<String> alternatives =
-          info.shorthand != null
-              ? ImmutableList.of(commandString(info.shorthand, info.paramLabels))
-              : ImmutableList.of();
+      List<String> alternatives;
+      if (info.shorthand != null) {
+        assert info.suffixLabel == null : "shorthands and prefixes cannot be combined.";
+        alternatives = ImmutableList.of(commandString(info.shorthand, null, info.paramLabels));
+      } else {
+        alternatives = ImmutableList.of();
+      }
       flags.add(
           new ParseFlagInfoImpl(
-              null, commandString(info.name, info.paramLabels), alternatives, helpLines));
+              null,
+              commandString(info.name, info.suffixLabel, info.paramLabels),
+              alternatives,
+              helpLines));
     }
     return flags;
   }
@@ -43,8 +49,11 @@
   }
 
   /** Returns a string like {@code --output <file>} */
-  private static String commandString(String name, List<String> paramLabels) {
+  private static String commandString(String name, String suffixLabel, List<String> paramLabels) {
     var sb = new StringBuilder(name);
+    if (suffixLabel != null) {
+      sb.append(suffixLabel);
+    }
     for (var label : paramLabels) {
       sb.append(' ').append(label);
     }
diff --git a/src/test/java/com/android/tools/r8/utils/CliParserTest.java b/src/test/java/com/android/tools/r8/utils/CliParserTest.java
index 7c51c88..9c1b966 100644
--- a/src/test/java/com/android/tools/r8/utils/CliParserTest.java
+++ b/src/test/java/com/android/tools/r8/utils/CliParserTest.java
@@ -164,6 +164,17 @@
   }
 
   @Test
+  public void testUsageMessageWithPrefix() {
+    CliParser<Builder> parser = new CliParser<>("Usage: test");
+    parser.prefix0("--flag", "<prop>", "Flag.", (b, s) -> {});
+    parser.prefix1("--mapping", "<key>", "<value>", "Mapping.", (b, s, v) -> {});
+
+    String usage = CliParserUtils.getUsageMessage(parser);
+    assertTrue(usage.contains("--flag<prop>"));
+    assertTrue(usage.contains("--mapping<key> <value>"));
+  }
+
+  @Test
   public void testInvalidOptionName() {
     CliParser<Builder> parser = new CliParser<>("Usage: test");
     assertThrows(
@@ -178,4 +189,125 @@
         AssertionError.class,
         () -> parser.option0("--help", "Help.", builder -> builder.help = true, "---h"));
   }
+
+  @Test
+  public void testPrefix1WithSpace() {
+    CliParser<Builder> parser = new CliParser<>("Usage: test");
+    List<String> mappings = new ArrayList<>();
+    parser.prefix1(
+        "--mapping",
+        "<key>",
+        "<value>",
+        "Mapping.",
+        (builder, suffix, value) -> {
+          mappings.add(suffix);
+          mappings.add(value);
+        });
+
+    Builder builder = new Builder();
+    List<String> errors = new ArrayList<>();
+    parser.parse(new String[] {"--mapping:fun", "a"}, builder, errors::add);
+
+    assertTrue(errors.isEmpty());
+    assertEquals(2, mappings.size());
+    assertEquals(":fun", mappings.get(0));
+    assertEquals("a", mappings.get(1));
+  }
+
+  @Test
+  public void testPrefix1WithEquals() {
+    CliParser<Builder> parser = new CliParser<>("Usage: test");
+    List<String> mappings = new ArrayList<>();
+    parser.prefix1(
+        "--mapping",
+        "<key>",
+        "<value>",
+        "Mapping.",
+        (builder, suffix, value) -> {
+          mappings.add(suffix);
+          mappings.add(value);
+        });
+
+    Builder builder = new Builder();
+    List<String> errors = new ArrayList<>();
+    parser.parse(new String[] {"--mapping:fun=a"}, builder, errors::add);
+
+    assertTrue(errors.isEmpty());
+    assertEquals(2, mappings.size());
+    assertEquals(":fun", mappings.get(0));
+    assertEquals("a", mappings.get(1));
+  }
+
+  @Test
+  public void testOverlapPrefixAndOption() {
+    CliParser<Builder> parser = new CliParser<>("Usage: test");
+    parser.prefix1("--test", "<key>", "<val>", "Prefix.", (b, s, v) -> {});
+    assertThrows(AssertionError.class, () -> parser.option0("--test-now", "Option.", b -> {}));
+  }
+
+  @Test
+  public void testOverlapOptionAndPrefix() {
+    CliParser<Builder> parser = new CliParser<>("Usage: test");
+    parser.option0("--test-now", "Option.", b -> {});
+    assertThrows(
+        AssertionError.class,
+        () -> parser.prefix1("--test", "<key>", "<val>", "Prefix.", (b, s, v) -> {}));
+  }
+
+  @Test
+  public void testOverlapPrefixAndPrefix() {
+    CliParser<Builder> parser = new CliParser<>("Usage: test");
+    parser.prefix1("--test", "<key>", "<val>", "Prefix.", (b, s, v) -> {});
+    assertThrows(
+        AssertionError.class,
+        () -> parser.prefix1("--test-now", "<key>", "<val>", "Prefix.", (b, s, v) -> {}));
+  }
+
+  @Test
+  public void testNoOverlapOptionAndPrefix() {
+    CliParser<Builder> parser = new CliParser<>("Usage: test");
+    parser.option0("--test", "Option.", b -> {});
+    parser.prefix1("--test-now", "<key>", "<val>", "Prefix.", (b, s, v) -> {});
+  }
+
+  @Test
+  public void testPrefix0() {
+    CliParser<Builder> parser = new CliParser<>("Usage: test");
+    List<String> suffixes = new ArrayList<>();
+    parser.prefix0(
+        "--flag", "<prop>", "Flag with suffix.", (builder, suffix) -> suffixes.add(suffix));
+
+    Builder builder = new Builder();
+    List<String> errors = new ArrayList<>();
+    parser.parse(new String[] {"--flag:foo"}, builder, errors::add);
+
+    assertTrue(errors.isEmpty());
+    assertEquals(1, suffixes.size());
+    assertEquals(":foo", suffixes.get(0));
+  }
+
+  @Test
+  public void testPrefix0WithEquals() {
+    CliParser<Builder> parser = new CliParser<>("Usage: test");
+    List<String> suffixes = new ArrayList<>();
+    parser.prefix0(
+        "--flag", "<prop>", "Flag with suffix.", (builder, suffix) -> suffixes.add(suffix));
+
+    Builder builder = new Builder();
+    List<String> errors = new ArrayList<>();
+    parser.parse(new String[] {"--flag:foo=bar"}, builder, errors::add);
+
+    assertEquals(1, errors.size());
+    assertEquals("Option --flag:foo does not take a value.", errors.get(0));
+    assertTrue(suffixes.isEmpty());
+  }
+
+  @Test
+  public void testOverlapPrefix0AndPrefix1() {
+    CliParser<Builder> parser = new CliParser<>("Usage: test");
+    parser.prefix0("--test", "<prop>", "Prefix 0.", (b, s) -> {});
+    assertThrows(
+        AssertionError.class,
+        () -> parser.prefix1("--test-now", "<key>", "<val>", "Prefix 1.", (b, s, v) -> {}));
+  }
 }
diff --git a/src/utils/java/com/android/tools/r8/utils/internal/CliParser.java b/src/utils/java/com/android/tools/r8/utils/internal/CliParser.java
index 5a40bf4..f5264b9 100644
--- a/src/utils/java/com/android/tools/r8/utils/internal/CliParser.java
+++ b/src/utils/java/com/android/tools/r8/utils/internal/CliParser.java
@@ -65,7 +65,7 @@
   }
 
   /**
-   * @param name must start with {@code --} and must be unique
+   * @param name must start with {@code --} and must be unique and non-overlapping
    * @param description must not contains line breaks, must end with {@code .}, and is automatically
    *     wrapped
    */
@@ -77,10 +77,10 @@
   }
 
   /**
-   * @param name must start with {@code --} and must be unique
+   * @param name must start with {@code --} and must be unique and non-overlapping
    * @param description must not contains line breaks, must end with {@code .}, and is automatically
    *     wrapped
-   * @param shorthand must start with {@code -} and must be unique
+   * @param shorthand must start with {@code -} and must be unique and non-overlapping
    */
   public CliParser<B> option0(
       String name, String description, Consumer<B> action, String shorthand) {
@@ -92,7 +92,7 @@
   }
 
   /**
-   * @param name must start with {@code --} and must be unique
+   * @param name must start with {@code --} and must be unique and non-overlapping
    * @param paramLabel must be surrounded by {@code <} and {@code >}
    * @param description must not contains line breaks, must end with {@code .}, and is automatically
    *     wrapped
@@ -107,11 +107,11 @@
   }
 
   /**
-   * @param name must start with {@code --} and must be unique
+   * @param name must start with {@code --} and must be unique and non-overlapping
    * @param paramLabel must be surrounded by {@code <} and {@code >}
    * @param description must not contains line breaks, must end with {@code .}, and is automatically
    *     wrapped
-   * @param shorthand must start with {@code -} and must be unique
+   * @param shorthand must start with {@code -} and must be unique and non-overlapping
    */
   public CliParser<B> option1(
       String name,
@@ -127,6 +127,40 @@
     return this;
   }
 
+  /**
+   * @param prefix must start with {@code --} and must be unique and non-overlapping
+   * @param description must not contains line breaks, must end with {@code .}, and is automatically
+   *     wrapped
+   */
+  public CliParser<B> prefix0(
+      String prefix, String suffixLabel, String description, BiConsumer<B, String> action) {
+    checkOptionName(prefix);
+    checkParam(suffixLabel);
+    checkDescription(description);
+    base.prefix0(prefix, suffixLabel, description, action);
+    return this;
+  }
+
+  /**
+   * @param prefix must start with {@code --} and must be unique and non-overlapping
+   * @param paramLabel must be surrounded by {@code <} and {@code >}
+   * @param description must not contains line breaks, must end with {@code .}, and is automatically
+   *     wrapped
+   */
+  public CliParser<B> prefix1(
+      String prefix,
+      String suffixLabel,
+      String paramLabel,
+      String description,
+      TriConsumer<B, String, String> action) {
+    checkOptionName(prefix);
+    checkParam(suffixLabel);
+    checkParam(paramLabel);
+    checkDescription(description);
+    base.prefix1(prefix, suffixLabel, paramLabel, description, action);
+    return this;
+  }
+
   private void checkOptionName(String name) {
     assert name.startsWith("--") : name + " does not start with --";
     assert name.length() > 2 : name + " is an empty name";
diff --git a/src/utils/java/com/android/tools/r8/utils/internal/CliParserBase.java b/src/utils/java/com/android/tools/r8/utils/internal/CliParserBase.java
index 399b422..1ee7de8 100644
--- a/src/utils/java/com/android/tools/r8/utils/internal/CliParserBase.java
+++ b/src/utils/java/com/android/tools/r8/utils/internal/CliParserBase.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils.internal;
 
+import com.android.tools.r8.utils.internal.collections.Pair;
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.Deque;
@@ -16,6 +17,8 @@
 
   private final Map<String, Consumer<B>> options0 = new HashMap<>();
   private final Map<String, BiConsumer<B, String>> options1 = new HashMap<>();
+  private final Map<String, BiConsumer<B, String>> prefix0 = new HashMap<>();
+  private final Map<String, TriConsumer<B, String, String>> prefix1 = new HashMap<>();
   private BiConsumer<B, String> positionalHandler;
   private final List<OptionInfo> optionInfos = new ArrayList<>();
   private final String usageHeader;
@@ -31,55 +34,61 @@
 
     public final String name;
     public final String shorthand;
+    public final String suffixLabel;
     public final ImmutableList<String> paramLabels;
     public final String description;
 
     OptionInfo(
-        String name, String shorthand, ImmutableList<String> paramLabels, String description) {
+        String name,
+        String shorthand,
+        String suffixLabel,
+        ImmutableList<String> paramLabels,
+        String description) {
       assert name != null;
       assert paramLabels != null;
       assert description != null;
       this.name = name;
       this.shorthand = shorthand;
+      this.suffixLabel = suffixLabel;
       this.paramLabels = paramLabels;
       this.description = description;
     }
   }
 
   /**
-   * @param name must be unique
+   * @param name must be unique and non-overlapping
    */
   public CliParserBase<B> option0(String name, String description, Consumer<B> action) {
     addOption0(name, action);
-    addHelp(name, null, ImmutableList.of(), description);
+    addHelp(name, null, null, ImmutableList.of(), description);
     return this;
   }
 
   /**
-   * @param name must be unique
-   * @param shorthand must be unique
+   * @param name must be unique and non-overlapping
+   * @param shorthand must be unique and non-overlapping
    */
   public CliParserBase<B> option0(
       String name, String description, Consumer<B> action, String shorthand) {
     addOption0(name, action);
     addOption0(shorthand, action);
-    addHelp(name, shorthand, ImmutableList.of(), description);
+    addHelp(name, shorthand, null, ImmutableList.of(), description);
     return this;
   }
 
   /**
-   * @param name must be unique
+   * @param name must be unique and non-overlapping
    */
   public CliParserBase<B> option1(
       String name, String paramLabel, String description, BiConsumer<B, String> action) {
     addOption1(name, action);
-    addHelp(name, null, ImmutableList.of(paramLabel), description);
+    addHelp(name, null, null, ImmutableList.of(paramLabel), description);
     return this;
   }
 
   /**
-   * @param name must be unique
-   * @param shorthand must be unique
+   * @param name must be unique and non-overlapping
+   * @param shorthand must be unique and non-overlapping
    */
   public CliParserBase<B> option1(
       String name,
@@ -89,7 +98,31 @@
       String shorthand) {
     addOption1(name, action);
     addOption1(shorthand, action);
-    addHelp(name, shorthand, ImmutableList.of(paramLabel), description);
+    addHelp(name, shorthand, null, ImmutableList.of(paramLabel), description);
+    return this;
+  }
+
+  /**
+   * @param prefix must start with {@code --} and must be unique and non-overlapping
+   */
+  public CliParserBase<B> prefix0(
+      String prefix, String suffixLabel, String description, BiConsumer<B, String> action) {
+    addPrefix0(prefix, action);
+    addHelp(prefix, null, suffixLabel, ImmutableList.of(), description);
+    return this;
+  }
+
+  /**
+   * @param prefix must start with {@code --} and must be unique and non-overlapping
+   */
+  public CliParserBase<B> prefix1(
+      String prefix,
+      String suffixLabel,
+      String paramLabel,
+      String description,
+      TriConsumer<B, String, String> action) {
+    addPrefix1(prefix, action);
+    addHelp(prefix, null, suffixLabel, ImmutableList.of(paramLabel), description);
     return this;
   }
 
@@ -114,6 +147,7 @@
     return ListUtils.unmodifiableForTesting(optionInfos);
   }
 
+  @SuppressWarnings("StatementWithEmptyBody")
   private void parseInternal(Deque<String> args, B builder, Consumer<String> errorReporter) {
     while (!args.isEmpty()) {
       String rawArg = args.removeFirst();
@@ -128,35 +162,101 @@
         }
       }
 
-      if (options0.containsKey(arg)) {
-        if (eqValue != null) {
-          errorReporter.accept("Option " + arg + " does not take a value.");
-        } else {
-          options0.get(arg).accept(builder);
-        }
-      } else if (options1.containsKey(arg)) {
-        if (eqValue != null) {
-          options1.get(arg).accept(builder, eqValue);
-        } else if (!args.isEmpty()) {
-          options1.get(arg).accept(builder, args.removeFirst());
-        } else {
-          errorReporter.accept("Missing parameter for " + arg + ".");
-          break;
-        }
-      } else if (positionalHandler != null) {
-        positionalHandler.accept(builder, rawArg);
+      if (tryParseOption0(arg, eqValue, builder, errorReporter)) {
+        // Matched.
+      } else if (tryParseOption1(arg, eqValue, args, builder, errorReporter)) {
+        // Matched.
+      } else if (tryParsePrefix0(arg, eqValue, builder, errorReporter)) {
+        // Matched.
+      } else if (tryParsePrefix1(arg, eqValue, args, builder, errorReporter)) {
+        // Matched.
+      } else if (tryParsePositional(rawArg, builder)) {
+        // Matched.
       } else {
         errorReporter.accept("Unexpected argument: " + rawArg);
       }
     }
   }
 
-  private boolean assertThatOptionIsNew(String name) {
-    assert !options0.containsKey(name) && !options1.containsKey(name)
-        : name + " is already an option";
+  private boolean tryParseOption0(
+      String arg, String eqValue, B builder, Consumer<String> errorReporter) {
+    if (!options0.containsKey(arg)) {
+      return false;
+    }
+    if (eqValue != null) {
+      errorReporter.accept("Option " + arg + " does not take a value.");
+    } else {
+      options0.get(arg).accept(builder);
+    }
     return true;
   }
 
+  private boolean tryParseOption1(
+      String arg, String eqValue, Deque<String> args, B builder, Consumer<String> errorReporter) {
+    if (!options1.containsKey(arg)) {
+      return false;
+    }
+    if (eqValue != null) {
+      options1.get(arg).accept(builder, eqValue);
+    } else if (!args.isEmpty()) {
+      options1.get(arg).accept(builder, args.removeFirst());
+    } else {
+      errorReporter.accept("Missing parameter for " + arg + ".");
+      args.clear();
+    }
+    return true;
+  }
+
+  private boolean tryParsePrefix0(
+      String arg, String eqValue, B builder, Consumer<String> errorReporter) {
+    Pair<String, BiConsumer<B, String>> match = findMatchedPrefix(arg, prefix0);
+    if (match == null) {
+      return false;
+    }
+    if (eqValue != null) {
+      errorReporter.accept("Option " + arg + " does not take a value.");
+    } else {
+      match.getSecond().accept(builder, match.getFirst());
+    }
+    return true;
+  }
+
+  private boolean tryParsePrefix1(
+      String arg, String eqValue, Deque<String> args, B builder, Consumer<String> errorReporter) {
+    Pair<String, TriConsumer<B, String, String>> match = findMatchedPrefix(arg, prefix1);
+    if (match == null) {
+      return false;
+    }
+    if (eqValue != null) {
+      match.getSecond().accept(builder, match.getFirst(), eqValue);
+    } else if (!args.isEmpty()) {
+      match.getSecond().accept(builder, match.getFirst(), args.removeFirst());
+    } else {
+      errorReporter.accept("Missing parameter for " + arg + ".");
+      args.clear();
+    }
+    return true;
+  }
+
+  private boolean tryParsePositional(String rawArg, B builder) {
+    if (positionalHandler == null) {
+      return false;
+    }
+    positionalHandler.accept(builder, rawArg);
+    return true;
+  }
+
+  /** The returned string in the pair is the suffix part of arg. */
+  private <V> Pair<String, V> findMatchedPrefix(String arg, Map<String, V> prefixMap) {
+    for (String prefix : prefixMap.keySet()) {
+      if (arg.startsWith(prefix)) {
+        String suffix = arg.substring(prefix.length());
+        return Pair.create(suffix, prefixMap.get(prefix));
+      }
+    }
+    return null;
+  }
+
   private void addOption0(String name, Consumer<B> action) {
     assert assertThatOptionIsNew(name);
     options0.put(name, action);
@@ -167,14 +267,66 @@
     options1.put(name, action);
   }
 
+  private void addPrefix0(String prefix, BiConsumer<B, String> action) {
+    assert assertThatPrefixIsNew(prefix);
+    prefix0.put(prefix, action);
+  }
+
+  private void addPrefix1(String prefix, TriConsumer<B, String, String> action) {
+    assert assertThatPrefixIsNew(prefix);
+    prefix1.put(prefix, action);
+  }
+
   private void addHelp(
-      String name, String shorthand, ImmutableList<String> paramLabels, String description) {
+      String name,
+      String shorthand,
+      String suffixLabel,
+      ImmutableList<String> paramLabels,
+      String description) {
     assert !name.contains("=") : name + " contains '='";
     if (shorthand != null) {
       assert !name.equals(shorthand) : "Shorthand is the same as the main name: " + name;
       assert !shorthand.contains("=") : shorthand + " contains '='";
     }
-    optionInfos.add(new OptionInfo(name, shorthand, paramLabels, description));
+    optionInfos.add(new OptionInfo(name, shorthand, suffixLabel, paramLabels, description));
+  }
+
+  private void forEachOption(Consumer<String> action) {
+    options0.keySet().forEach(action);
+    options1.keySet().forEach(action);
+  }
+
+  private void forEachPrefix(Consumer<String> action) {
+    prefix0.keySet().forEach(action);
+    prefix1.keySet().forEach(action);
+  }
+
+  private boolean assertThatOptionIsNew(String name) {
+    forEachOption(
+        existing -> {
+          assert !name.equals(existing)
+              : "Overlap detected: Option " + name + " and option " + existing;
+        });
+    forEachPrefix(
+        existing -> {
+          assert !name.startsWith(existing)
+              : "Overlap detected: Option " + name + " and prefix " + existing;
+        });
+    return true;
+  }
+
+  private boolean assertThatPrefixIsNew(String name) {
+    forEachOption(
+        existing -> {
+          assert !existing.startsWith(name)
+              : "Overlap detected: Prefix " + name + " and option " + existing;
+        });
+    forEachPrefix(
+        existing -> {
+          assert !name.startsWith(existing) && !existing.startsWith(name)
+              : "Overlap detected: Prefix " + name + " and prefix " + existing;
+        });
+    return true;
   }
 
   private boolean assertValidPositional() {