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() {