Add expansion functions to options parser

This provides a way to programmatically define expansions of options based on what other options are defined for the parser. In particular, it will be used for the --incompatible_* changes mechanism, to turn on all incompatible change flags.

Expansion functions are specified in the @Option annotation, similar to converters. They are computed when an OptionsParser is constructed, and inspect a preliminary version of its OptionsData to determine the expansion result. This is then cached in the final OptionsData used by the parser.

Expansion information for usage strings is available, but only when the usage strings are obtained via the parser.

--
PiperOrigin-RevId: 150817553
MOS_MIGRATED_REVID=150817553
diff --git a/src/test/java/com/google/devtools/common/options/OptionsTest.java b/src/test/java/com/google/devtools/common/options/OptionsTest.java
index fccc011..21a8a06 100644
--- a/src/test/java/com/google/devtools/common/options/OptionsTest.java
+++ b/src/test/java/com/google/devtools/common/options/OptionsTest.java
@@ -16,15 +16,14 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static java.util.Arrays.asList;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import com.google.common.collect.Iterables;
 import com.google.common.testing.EqualsTester;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.util.Map;
+import java.util.TreeSet;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -66,6 +65,31 @@
             defaultValue = "null",
             expansion = { "--host=special.google.com", "--port=8080"})
     public Void special;
+
+    // Interestingly, the class needs to be public, or else the default constructor ends up not
+    // being public and the expander can't be instantiated.
+    /** SpecialExpansion */
+    public static class SpecialExpansion implements ExpansionFunction {
+      @Override
+      public String[] getExpansion(IsolatedOptionsData optionsData) {
+        TreeSet<String> flags = new TreeSet<>();
+        for (Map.Entry<String, ?> entry : optionsData.getAllNamedFields()) {
+          if (entry.getKey().startsWith("specialexp_")) {
+            flags.add("--" + entry.getKey());
+          }
+        }
+        return Iterables.toArray(flags, String.class);
+      }
+    }
+
+    @Option(name = "specialexp_foo", defaultValue = "false")
+    public boolean specialExpFoo;
+
+    @Option(name = "specialexp_bar", defaultValue = "false")
+    public boolean specialExpBar;
+
+    @Option(name = "specialexp", defaultValue = "null", expansionFunction = SpecialExpansion.class)
+    public Void specialExp;
   }
 
   @Test
@@ -73,15 +97,14 @@
     // TODO(bazel-team): don't include trailing space after last word in line.
     String input = "The quick brown fox jumps over the lazy dog.";
 
-    assertEquals("  The quick \n  brown fox \n  jumps over \n  the lazy \n"
-                 + "  dog.",
-                 OptionsUsage.paragraphFill(input, 2, 13));
-    assertEquals("   The quick brown \n   fox jumps over \n   the lazy dog.",
-                 OptionsUsage.paragraphFill(input, 3, 19));
+    assertThat(OptionsUsage.paragraphFill(input, 2, 13))
+        .isEqualTo("  The quick \n  brown fox \n  jumps over \n  the lazy \n" + "  dog.");
+    assertThat(OptionsUsage.paragraphFill(input, 3, 19))
+        .isEqualTo("   The quick brown \n   fox jumps over \n   the lazy dog.");
 
     String input2 = "The quick brown fox jumps\nAnother paragraph.";
-    assertEquals("  The quick brown fox \n  jumps\n  Another paragraph.",
-                 OptionsUsage.paragraphFill(input2, 2, 23));
+    assertThat(OptionsUsage.paragraphFill(input2, 2, 23))
+        .isEqualTo("  The quick brown fox \n  jumps\n  Another paragraph.");
   }
 
   @Test
@@ -90,11 +113,11 @@
     String[] remainingArgs = options.getRemainingArgs();
     HttpOptions webFlags = options.getOptions();
 
-    assertEquals("www.google.com", webFlags.host);
-    assertEquals(80, webFlags.port);
-    assertEquals(false, webFlags.isDebugging);
-    assertEquals(TriState.AUTO, webFlags.triState);
-    assertEquals(0, remainingArgs.length);
+    assertThat(webFlags.host).isEqualTo("www.google.com");
+    assertThat(webFlags.port).isEqualTo(80);
+    assertThat(webFlags.isDebugging).isEqualTo(false);
+    assertThat(webFlags.triState).isEqualTo(TriState.AUTO);
+    assertThat(remainingArgs.length).isEqualTo(0);
   }
 
   @Test
@@ -110,28 +133,27 @@
 
     String toString = left.toString();
     // Don't rely on Set.toString iteration order:
-    assertTrue(toString.startsWith(
-                   "com.google.devtools.common.options.OptionsTest"
-                   + "$HttpOptions{"));
-    assertTrue(toString.contains("host=foo"));
-    assertTrue(toString.contains("port=80"));
-    assertTrue(toString.endsWith("}"));
+    assertThat(toString)
+        .startsWith("com.google.devtools.common.options.OptionsTest" + "$HttpOptions{");
+    assertThat(toString).contains("host=foo");
+    assertThat(toString).contains("port=80");
+    assertThat(toString).endsWith("}");
 
     new EqualsTester().addEqualityGroup(left).testEquals();
-    assertTrue(left.toString().equals(likeLeft.toString()));
-    assertTrue(left.equals(likeLeft));
-    assertTrue(likeLeft.equals(left));
-    assertFalse(left.equals(right));
-    assertFalse(right.equals(left));
-    assertFalse(left.equals(null));
-    assertFalse(likeLeft.equals(null));
-    assertEquals(likeLeft.hashCode(), likeLeft.hashCode());
-    assertEquals(left.hashCode(), likeLeft.hashCode());
+    assertThat(left.toString()).isEqualTo(likeLeft.toString());
+    assertThat(left).isEqualTo(likeLeft);
+    assertThat(likeLeft).isEqualTo(left);
+    assertThat(left).isNotEqualTo(right);
+    assertThat(right).isNotEqualTo(left);
+    assertThat(left).isNotEqualTo(null);
+    assertThat(likeLeft).isNotEqualTo(null);
+    assertThat(likeLeft.hashCode()).isEqualTo(likeLeft.hashCode());
+    assertThat(likeLeft.hashCode()).isEqualTo(left.hashCode());
     // Strictly speaking this is not required for hashCode to be correct,
     // but a good hashCode should be different at least for some values. So,
     // we're making sure that at least this particular pair of inputs yields
     // different values.
-    assertFalse(left.hashCode() == right.hashCode());
+    assertThat(left.hashCode()).isNotEqualTo(right.hashCode());
   }
 
   @Test
@@ -141,11 +163,12 @@
 
     String[] args2 = { "-p", "80", "--host", "foo" };
     HttpOptions options2 =  Options.parse(HttpOptions.class, args2).getOptions();
-    assertEquals("order/abbreviations shouldn't matter", options1, options2);
+    // Order/abbreviations shouldn't matter.
+    assertThat(options1).isEqualTo(options2);
 
-    assertEquals("explicitly setting a default shouldn't matter",
-        Options.parse(HttpOptions.class, "--port", "80").getOptions(),
-        Options.parse(HttpOptions.class).getOptions());
+    // Explicitly setting a default shouldn't matter.
+    assertThat(Options.parse(HttpOptions.class, "--port", "80").getOptions())
+        .isEqualTo(Options.parse(HttpOptions.class).getOptions());
 
     assertThat(Options.parse(HttpOptions.class, "--port", "3").getOptions())
         .isNotEqualTo(Options.parse(HttpOptions.class).getOptions());
@@ -161,10 +184,10 @@
     String[] remainingArgs = options.getRemainingArgs();
     HttpOptions webFlags = options.getOptions();
 
-    assertEquals("google.com", webFlags.host);
-    assertEquals(8080, webFlags.port);
-    assertEquals(true, webFlags.isDebugging);
-    assertEquals(0, remainingArgs.length);
+    assertThat(webFlags.host).isEqualTo("google.com");
+    assertThat(webFlags.port).isEqualTo(8080);
+    assertThat(webFlags.isDebugging).isEqualTo(true);
+    assertThat(remainingArgs.length).isEqualTo(0);
   }
 
   @Test
@@ -176,10 +199,10 @@
     String[] remainingArgs = options.getRemainingArgs();
     HttpOptions webFlags = options.getOptions();
 
-    assertEquals("google.com", webFlags.host);
-    assertEquals(8080, webFlags.port);
-    assertEquals(true, webFlags.isDebugging);
-    assertEquals(0, remainingArgs.length);
+    assertThat(webFlags.host).isEqualTo("google.com");
+    assertThat(webFlags.port).isEqualTo(8080);
+    assertThat(webFlags.isDebugging).isEqualTo(true);
+    assertThat(remainingArgs.length).isEqualTo(0);
   }
 
   @Test
@@ -187,8 +210,8 @@
     Options<HttpOptions> options =
         Options.parse(HttpOptions.class, new String[]{"--nodebug", "--notristate"});
     HttpOptions webFlags = options.getOptions();
-    assertEquals(false, webFlags.isDebugging);
-    assertEquals(TriState.NO, webFlags.triState);
+    assertThat(webFlags.isDebugging).isEqualTo(false);
+    assertThat(webFlags.triState).isEqualTo(TriState.NO);
   }
 
   @Test
@@ -196,8 +219,8 @@
     Options<HttpOptions> options =
         Options.parse(HttpOptions.class, new String[]{"--no_debug", "--no_tristate"});
     HttpOptions webFlags = options.getOptions();
-    assertEquals(false, webFlags.isDebugging);
-    assertEquals(TriState.NO, webFlags.triState);
+    assertThat(webFlags.isDebugging).isEqualTo(false);
+    assertThat(webFlags.triState).isEqualTo(TriState.NO);
   }
 
   @Test
@@ -205,8 +228,8 @@
     Options<HttpOptions> options =
         Options.parse(HttpOptions.class, new String[]{"-d-", "-t-"});
     HttpOptions webFlags = options.getOptions();
-    assertEquals(false, webFlags.isDebugging);
-    assertEquals(TriState.NO, webFlags.triState);
+    assertThat(webFlags.isDebugging).isEqualTo(false);
+    assertThat(webFlags.triState).isEqualTo(TriState.NO);
   }
 
   @Test
@@ -214,8 +237,8 @@
     Options<HttpOptions> options =
         Options.parse(HttpOptions.class, new String[]{"--debug=0", "--tristate=0"});
     HttpOptions webFlags = options.getOptions();
-    assertEquals(false, webFlags.isDebugging);
-    assertEquals(TriState.NO, webFlags.triState);
+    assertThat(webFlags.isDebugging).isEqualTo(false);
+    assertThat(webFlags.triState).isEqualTo(TriState.NO);
   }
 
   @Test
@@ -223,8 +246,8 @@
     Options<HttpOptions> options =
         Options.parse(HttpOptions.class, new String[]{"--debug=1", "--tristate=1"});
     HttpOptions webFlags = options.getOptions();
-    assertEquals(true, webFlags.isDebugging);
-    assertEquals(TriState.YES, webFlags.triState);
+    assertThat(webFlags.isDebugging).isEqualTo(true);
+    assertThat(webFlags.triState).isEqualTo(TriState.YES);
   }
 
   @Test
@@ -232,7 +255,7 @@
     String[] args = {"these", "aint", "options"};
     Options<HttpOptions> options = Options.parse(HttpOptions.class, args);
     String[] remainingArgs = options.getRemainingArgs();
-    assertEquals(asList(args), asList(remainingArgs));
+    assertThat(asList(remainingArgs)).isEqualTo(asList(args));
   }
 
   @Test
@@ -246,7 +269,7 @@
     String[] notoptions = {"notta", "option" };
     Options<HttpOptions> options = Options.parse(HttpOptions.class, args);
     String[] remainingArgs = options.getRemainingArgs();
-    assertEquals(asList(notoptions), asList(remainingArgs));
+    assertThat(asList(remainingArgs)).isEqualTo(asList(notoptions));
   }
 
   @Test
@@ -256,7 +279,7 @@
       Options.parse(HttpOptions.class, args);
       fail();
     } catch (OptionsParsingException e) {
-      assertEquals("Unrecognized option: --unknown", e.getMessage());
+      assertThat(e.getMessage()).isEqualTo("Unrecognized option: --unknown");
     }
   }
 
@@ -267,7 +290,7 @@
       Options.parse(HttpOptions.class, args);
       fail();
     } catch (OptionsParsingException e) {
-      assertEquals("Expected value after --port", e.getMessage());
+      assertThat(e.getMessage()).isEqualTo("Expected value after --port");
     }
   }
 
@@ -276,7 +299,7 @@
     String[] args = {"--port=80", "--port", "81"};
     Options<HttpOptions> options = Options.parse(HttpOptions.class, args);
     HttpOptions webFlags = options.getOptions();
-    assertEquals(81, webFlags.port);
+    assertThat(webFlags.port).isEqualTo(81);
   }
 
   @Test
@@ -284,7 +307,7 @@
     String[] args = {"--port=80", "-p", "81"};
     Options<HttpOptions> options = Options.parse(HttpOptions.class, args);
     HttpOptions webFlags = options.getOptions();
-    assertEquals(81, webFlags.port);
+    assertThat(webFlags.port).isEqualTo(81);
   }
 
   @Test
@@ -300,8 +323,10 @@
       Options.parse(HttpOptions.class, new String[]{"--debug=not_a_boolean"});
       fail();
     } catch (OptionsParsingException e) {
-      assertEquals("While parsing option --debug=not_a_boolean: "
-                   + "\'not_a_boolean\' is not a boolean", e.getMessage());
+      assertThat(e.getMessage())
+          .isEqualTo(
+              "While parsing option --debug=not_a_boolean: "
+                  + "\'not_a_boolean\' is not a boolean");
     }
   }
 
@@ -311,29 +336,38 @@
       Options.parse(HttpOptions.class, new String[]{"--nodebug=1"});
       fail();
     } catch (OptionsParsingException e) {
-      assertEquals("Unexpected value after boolean option: --nodebug=1", e.getMessage());
+      assertThat(e.getMessage()).isEqualTo("Unexpected value after boolean option: --nodebug=1");
     }
   }
 
   @Test
-  public void usageForBuiltinTypes() {
+  public void usageForBuiltinTypesNoExpansion() {
     String usage = Options.getUsage(HttpOptions.class);
     // We can't rely on the option ordering.
-    assertTrue(usage.contains(
-            "  --[no]debug [-d] (a boolean; default: \"false\")\n" +
-            "    debug"));
-    assertTrue(usage.contains(
-            "  --host (a string; default: \"www.google.com\")\n" +
-            "    The URL at which the server will be running."));
-    assertTrue(usage.contains(
-            "  --port [-p] (an integer; default: \"80\")\n" +
-            "    The port at which the server will be running."));
-    assertTrue(usage.contains(
-            "  --special\n" +
-            "    Expands to: --host=special.google.com --port=8080"));
-    assertTrue(usage.contains(
-        "  --[no]tristate [-t] (a tri-state (auto, yes, no); default: \"auto\")\n" +
-        "    tri-state option returning auto by default"));
+    assertThat(usage)
+        .contains("  --[no]debug [-d] (a boolean; default: \"false\")\n" + "    debug");
+    assertThat(usage)
+        .contains(
+            "  --host (a string; default: \"www.google.com\")\n"
+                + "    The URL at which the server will be running.");
+    assertThat(usage)
+        .contains(
+            "  --port [-p] (an integer; default: \"80\")\n"
+                + "    The port at which the server will be running.");
+    assertThat(usage)
+        .contains(
+            "  --[no]tristate [-t] (a tri-state (auto, yes, no); default: \"auto\")\n"
+                + "    tri-state option returning auto by default");
+  }
+
+  @Test
+  public void usageForExpansion() {
+    String usage = Options.getUsage(HttpOptions.class);
+    assertThat(usage)
+        .contains("  --special\n    Expands to: --host=special.google.com --port=8080");
+    // Expansion functions aren't evaluated since we're just grabbing the usage for an OptionsBase
+    // subclass and not for a completed parser. The completed case is covered in OptionsParserTest.
+    assertThat(usage).contains("  --specialexp\n    Expands to unknown options.");
   }
 
   public static class NullTestOptions extends OptionsBase {
@@ -352,13 +386,13 @@
   @Test
   public void usageForNullDefault() {
     String usage = Options.getUsage(NullTestOptions.class);
-    assertTrue(usage.contains(
-            "  --host (a string; default: see description)\n" +
-            "    The URL at which the server will be running."));
-    assertTrue(usage.contains(
-            "  --none\n" +
-            "    An expanded option.\n" +
-            "    Expands to: --host=www.google.com"));
+    assertThat(usage)
+        .contains(
+            "  --host (a string; default: see description)\n"
+                + "    The URL at which the server will be running.");
+    assertThat(usage)
+        .contains(
+            "  --none\n" + "    An expanded option.\n" + "    Expands to: --host=www.google.com");
   }
 
   public static class MyURLConverter implements Converter<URL> {
@@ -394,7 +428,7 @@
     Options<UsesCustomConverter> options =
       Options.parse(UsesCustomConverter.class, new String[0]);
     URL expected = new URL("http://www.google.com/");
-    assertEquals(expected, options.getOptions().url);
+    assertThat(options.getOptions().url).isEqualTo(expected);
   }
 
   @Test
@@ -404,17 +438,18 @@
       Options.parse(UsesCustomConverter.class, args);
       fail();
     } catch (OptionsParsingException e) {
-      assertEquals("While parsing option --url=a_malformed:url: "
-                   + "Could not convert 'a_malformed:url': "
-                   + "no protocol: a_malformed:url", e.getMessage());
+      assertThat(e.getMessage())
+          .isEqualTo(
+              "While parsing option --url=a_malformed:url: "
+                  + "Could not convert 'a_malformed:url': "
+                  + "no protocol: a_malformed:url");
     }
   }
 
   @Test
   public void usageWithCustomConverter() {
-    assertEquals(
-        "  --url (a url; default: \"http://www.google.com/\")\n",
-        Options.getUsage(UsesCustomConverter.class));
+    assertThat(Options.getUsage(UsesCustomConverter.class))
+        .isEqualTo("  --url (a url; default: \"http://www.google.com/\")\n");
   }
 
   @Test
@@ -423,7 +458,7 @@
       Options.parse(HttpOptions.class, new String[]{"--no-debug"});
       fail();
     } catch (OptionsParsingException e) {
-      assertEquals("Unrecognized option: --no-debug", e.getMessage());
+      assertThat(e.getMessage()).isEqualTo("Unrecognized option: --no-debug");
     }
   }
 
@@ -434,7 +469,7 @@
   @Test
   public void nullDefaultForReferenceTypeOption() throws Exception {
     J options = Options.parse(J.class, NO_ARGS).getOptions();
-    assertNull(options.string);
+    assertThat(options.string).isNull();
   }
 
   public static class K extends OptionsBase {
@@ -451,9 +486,10 @@
       Options.parse(K.class, NO_ARGS).getOptions();
       fail();
     } catch (IllegalStateException e) {
-      assertEquals("OptionsParsingException while retrieving default for "
-                   + "int1: 'null' is not an int",
-                   e.getMessage());
+      assertThat(e.getMessage())
+          .isEqualTo(
+              "OptionsParsingException while retrieving default for "
+                  + "int1: 'null' is not an int");
     }
   }
 
@@ -463,30 +499,30 @@
     HttpOptions options =
         Options.parse(HttpOptions.class,
                       new String[] { "--host", "null" }).getOptions();
-    assertEquals("null", options.host);
+    assertThat(options.host).isEqualTo("null");
   }
 
   @Test
   public void nonDecimalRadicesForIntegerOptions() throws Exception {
     Options<HttpOptions> options =
         Options.parse(HttpOptions.class, new String[] { "--port", "0x51"});
-    assertEquals(81, options.getOptions().port);
+    assertThat(options.getOptions().port).isEqualTo(81);
   }
 
   @Test
   public void expansionOptionSimple() throws Exception {
     Options<HttpOptions> options =
       Options.parse(HttpOptions.class, new String[] {"--special"});
-    assertEquals("special.google.com", options.getOptions().host);
-    assertEquals(8080, options.getOptions().port);
+    assertThat(options.getOptions().host).isEqualTo("special.google.com");
+    assertThat(options.getOptions().port).isEqualTo(8080);
   }
 
   @Test
   public void expansionOptionOverride() throws Exception {
     Options<HttpOptions> options =
       Options.parse(HttpOptions.class, new String[] {"--port=90", "--special", "--host=foo"});
-    assertEquals("foo", options.getOptions().host);
-    assertEquals(8080, options.getOptions().port);
+    assertThat(options.getOptions().host).isEqualTo("foo");
+    assertThat(options.getOptions().port).isEqualTo(8080);
   }
 
   @Test
@@ -495,6 +531,14 @@
       Options.parse(HttpOptions.class, new String[] { "--host=special.google.com", "--port=8080"});
     Options<HttpOptions> options2 =
       Options.parse(HttpOptions.class, new String[] { "--special" });
-    assertEquals(options1.getOptions(), options2.getOptions());
+    assertThat(options1.getOptions()).isEqualTo(options2.getOptions());
+  }
+
+  @Test
+  public void expansionFunction() throws Exception {
+    Options<HttpOptions> options1 = Options.parse(HttpOptions.class, new String[] {"--specialexp"});
+    Options<HttpOptions> options2 =
+        Options.parse(HttpOptions.class, new String[] {"--specialexp_foo", "--specialexp_bar"});
+    assertThat(options1.getOptions()).isEqualTo(options2.getOptions());
   }
 }