blaze config: include "fragment -> [fragment options] map".

This is necessary to link options to the fragment(s) that contain them.

For example, if two configurations have a different --linkopt value, this belongs to the CppOptions FragmentOptions, which is included with the CppConfiguration fragment.

Serves https://github.com/bazelbuild/bazel/issues/10613.

Example output:

$ blaze config <config hash>
INFO: Displaying config with id cc19b5007
BuildConfiguration  cc19b5007
Skyframe Key: BuildConfigurationValue.Key[cc19b5007]
Fragments: com.google.devtools.build.lib.analysis.PlatformConfiguration: [com.google.devtools.build.lib.analysis.PlatformOptions], com.google.devtools.build.lib.analysis.ShellConfiguration:[com.google.devtools.build.lib.analysis.ShellConfiguration$Options], ...
FragmentOptions com.google.devtools.build.lib.analysis.PlatformOptions {
  enabled_toolchain_types: []
  experimental_add_exec_constraints_to_targets: []
  extra_toolchains: []
  incompatible_auto_configure_host_platform: false
  incompatible_use_toolchain_resolution_for_java_rules: true
  platform_mappings: tools/platform_mappings
...
PiperOrigin-RevId: 301158932
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/ConfigCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/ConfigCommand.java
index 3365bc7..355ec3b 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/ConfigCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/ConfigCommand.java
@@ -16,6 +16,7 @@
 import static com.google.common.collect.ImmutableSortedMap.toImmutableSortedMap;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Verify;
 import com.google.common.collect.HashBasedTable;
@@ -25,7 +26,10 @@
 import com.google.common.collect.Ordering;
 import com.google.common.collect.Sets;
 import com.google.common.collect.Table;
+import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
 import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment;
+import com.google.devtools.build.lib.analysis.config.ConfigurationFragmentFactory;
 import com.google.devtools.build.lib.analysis.config.CoreOptions;
 import com.google.devtools.build.lib.analysis.config.FragmentOptions;
 import com.google.devtools.build.lib.cmdline.Label;
@@ -58,7 +62,6 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
-import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
 /** Handles the 'config' command on the Blaze command line. */
@@ -108,7 +111,7 @@
   }
 
   /**
-   * Data structure defining a {@link BuildConfiguration} from the point of this command's output.
+   * Data structure defining a {@link BuildConfiguration} for the purpose of this command's output.
    *
    * <p>Includes all data representing a "configuration" and defines their relative structure and
    * list order.
@@ -120,11 +123,17 @@
     final String skyKey;
     final String configHash;
     final List<FragmentForOutput> fragments;
+    final List<FragmentOptionsForOutput> fragmentOptions;
 
-    ConfigurationForOutput(String skyKey, String configHash, List<FragmentForOutput> fragments) {
+    ConfigurationForOutput(
+        String skyKey,
+        String configHash,
+        List<FragmentForOutput> fragments,
+        List<FragmentOptionsForOutput> fragmentOptions) {
       this.skyKey = skyKey;
       this.configHash = configHash;
       this.fragments = fragments;
+      this.fragmentOptions = fragmentOptions;
     }
 
     @Override
@@ -133,35 +142,72 @@
         ConfigurationForOutput other = (ConfigurationForOutput) o;
         return other.skyKey.equals(skyKey)
             && other.configHash.equals(configHash)
-            && other.fragments.equals(fragments);
+            && other.fragments.equals(fragments)
+            && other.fragmentOptions.equals(fragmentOptions);
       }
       return false;
     }
 
     @Override
     public int hashCode() {
-      return Objects.hash(skyKey, configHash, fragments);
+      return Objects.hash(skyKey, configHash, fragments, fragmentOptions);
     }
   }
 
   /**
-   * Data structure defining a {@link FragmentOptions} from the point of this command's output.
+   * Data structure defining a {@link Fragment} for the purpose of this command's output.
    *
-   * <p>See {@link ConfigurationForOutput} for further details.
+   * <p>{@link Fragment} is a Java object representation of a domain-specific "piece" of
+   * configuration (like "C++-related configuration"). It depends on one or more {@link
+   * FragmentOptions}, which are the <code>--flag=value</code> pairs that key configurations.
+   *
+   * <p>See {@link FragmentOptionsForOutput} and {@link ConfigurationForOutput} for further details.
    */
   protected static class FragmentForOutput {
     final String name;
-    final Map<String, String> options;
+    // We store the name of the associated FragmentOptions instead of FragmentOptionsForOutput
+    // objects because multiple fragments may use the same FragmentOptions and we don't want to list
+    // it multiple times.
+    final List<String> fragmentOptions;
 
-    FragmentForOutput(String name, Map<String, String> options) {
+    FragmentForOutput(String name, List<String> fragmentOptions) {
       this.name = name;
-      this.options = options;
+      this.fragmentOptions = fragmentOptions;
     }
 
     @Override
     public boolean equals(Object o) {
       if (o instanceof FragmentForOutput) {
         FragmentForOutput other = (FragmentForOutput) o;
+        return other.name.equals(name) && other.fragmentOptions.equals(fragmentOptions);
+      }
+      return false;
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(name, fragmentOptions);
+    }
+  }
+
+  /**
+   * Data structure defining a {@link FragmentOptions} from the point of this command's output.
+   *
+   * <p>See {@link FragmentForOutput} and {@link ConfigurationForOutput} for further details.
+   */
+  protected static class FragmentOptionsForOutput {
+    final String name;
+    final Map<String, String> options;
+
+    FragmentOptionsForOutput(String name, Map<String, String> options) {
+      this.name = name;
+      this.options = options;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (o instanceof FragmentOptionsForOutput) {
+        FragmentOptionsForOutput other = (FragmentOptionsForOutput) o;
         return other.name.equals(name) && other.options.equals(options);
       }
       return false;
@@ -210,7 +256,7 @@
 
   /**
    * Data structure defining the difference between two {@link BuildConfiguration}s for a given
-   * {@link FragmentOptions }from the point of this command's output.
+   * {@link FragmentOptions}from the point of this command's output.
    *
    * <p>See {@link ConfigurationForOutput} for further details.
    */
@@ -258,17 +304,20 @@
           configCommandOptions.outputType == OutputType.TEXT
               ? new TextOutputFormatter(writer)
               : new JsonOutputFormatter(writer);
+      ImmutableSortedMap<
+              Class<? extends Fragment>, ImmutableSortedSet<Class<? extends FragmentOptions>>>
+          fragmentDefs = getFragmentDefs(env.getRuntime().getRuleClassProvider());
 
       if (options.getResidue().isEmpty()) {
         if (configCommandOptions.dumpAll) {
-          return reportAllConfigurations(outputFormatter, forOutput(configurations));
+          return reportAllConfigurations(outputFormatter, forOutput(configurations, fragmentDefs));
         } else {
-          return reportConfigurationIds(outputFormatter, forOutput(configurations));
+          return reportConfigurationIds(outputFormatter, forOutput(configurations, fragmentDefs));
         }
       } else if (options.getResidue().size() == 1) {
         String configHash = options.getResidue().get(0);
         return reportSingleConfiguration(
-            outputFormatter, env, forOutput(configurations), configHash);
+            outputFormatter, env, forOutput(configurations, fragmentDefs), configHash);
       } else if (options.getResidue().size() == 2) {
         String configHash1 = options.getResidue().get(0);
         String configHash2 = options.getResidue().get(1);
@@ -300,41 +349,98 @@
   }
 
   /**
+   * Returns the {@link Fragment}s and the {@link FragmentOptions} they require from Blaze's
+   * runtime.
+   *
+   * <p>These are the fragments that Blaze "knows about", not necessarily the fragments in a {@link
+   * BuildConfiguration}. Trimming, in particular, strips fragments out of actual configurations.
+   * It's safe to assume untrimmed configuration have all fragments listed here.
+   */
+  private static ImmutableSortedMap<
+          Class<? extends Fragment>, ImmutableSortedSet<Class<? extends FragmentOptions>>>
+      getFragmentDefs(ConfiguredRuleClassProvider ruleClassProvider) {
+    ImmutableSortedMap.Builder<
+            Class<? extends Fragment>, ImmutableSortedSet<Class<? extends FragmentOptions>>>
+        fragments = ImmutableSortedMap.orderedBy((c1, c2) -> c1.getName().compareTo(c2.getName()));
+    for (ConfigurationFragmentFactory fragmentFactory :
+        ruleClassProvider.getConfigurationFragments()) {
+      fragments.put(
+          fragmentFactory.creates(),
+          ImmutableSortedSet.copyOf(
+              (c1, c2) -> c1.getName().compareTo(c2.getName()), fragmentFactory.requiredOptions()));
+    }
+    return fragments.build();
+  }
+
+  /**
    * Converts {@link #findConfigurations}'s output into a list of {@link ConfigurationForOutput}
    * instances.
    */
-  private ImmutableSortedSet<ConfigurationForOutput> forOutput(
-      ImmutableSortedMap<BuildConfigurationValue.Key, BuildConfiguration> asSkyKeyMap) {
+  private static ImmutableSortedSet<ConfigurationForOutput> forOutput(
+      ImmutableSortedMap<BuildConfigurationValue.Key, BuildConfiguration> asSkyKeyMap,
+      ImmutableSortedMap<
+              Class<? extends Fragment>, ImmutableSortedSet<Class<? extends FragmentOptions>>>
+          fragmentDefs) {
     ImmutableSortedSet.Builder<ConfigurationForOutput> ans =
         ImmutableSortedSet.orderedBy(comparing(e -> e.configHash));
     for (Map.Entry<BuildConfigurationValue.Key, BuildConfiguration> entry :
         asSkyKeyMap.entrySet()) {
       BuildConfigurationValue.Key key = entry.getKey();
       BuildConfiguration config = entry.getValue();
-      ans.add(getConfigurationForOutput(key, config.checksum(), config));
+      ans.add(getConfigurationForOutput(key, config.checksum(), config, fragmentDefs));
     }
     return ans.build();
   }
 
   /** Constructs a {@link ConfigurationForOutput} from the given input daata. */
-  ConfigurationForOutput getConfigurationForOutput(
-      BuildConfigurationValue.Key skyKey, String configHash, BuildConfiguration config) {
+  private static ConfigurationForOutput getConfigurationForOutput(
+      BuildConfigurationValue.Key skyKey,
+      String configHash,
+      BuildConfiguration config,
+      ImmutableSortedMap<
+              Class<? extends Fragment>, ImmutableSortedSet<Class<? extends FragmentOptions>>>
+          fragmentDefs) {
+
     ImmutableSortedSet.Builder<FragmentForOutput> fragments =
         ImmutableSortedSet.orderedBy(comparing(e -> e.name));
+    for (Map.Entry<Class<? extends Fragment>, ImmutableSortedSet<Class<? extends FragmentOptions>>>
+        entry : fragmentDefs.entrySet()) {
+      fragments.add(
+          new FragmentForOutput(
+              entry.getKey().getName(),
+              entry.getValue().stream().map(clazz -> clazz.getName()).collect(toList())));
+    }
+    fragmentDefs.entrySet().stream()
+        .filter(entry -> config.hasFragment(entry.getKey()))
+        .forEach(
+            entry ->
+                fragments.add(
+                    new FragmentForOutput(
+                        entry.getKey().getName(),
+                        entry.getValue().stream()
+                            .map(clazz -> clazz.getName())
+                            .collect(toList()))));
+
+    ImmutableSortedSet.Builder<FragmentOptionsForOutput> fragmentOptions =
+        ImmutableSortedSet.orderedBy(comparing(e -> e.name));
     config.getOptions().getFragmentClasses().stream()
         .map(optionsClass -> config.getOptions().get(optionsClass))
         .forEach(
-            fragmentOptions -> {
-              fragments.add(
-                  new FragmentForOutput(
-                      fragmentOptions.getClass().getName(),
-                      getOrderedNativeOptions(fragmentOptions)));
+            fragmentOptionsInstance -> {
+              fragmentOptions.add(
+                  new FragmentOptionsForOutput(
+                      fragmentOptionsInstance.getClass().getName(),
+                      getOrderedNativeOptions(fragmentOptionsInstance)));
             });
-    fragments.add(
-        new FragmentForOutput(
+    fragmentOptions.add(
+        new FragmentOptionsForOutput(
             UserDefinedFragment.DESCRIPTIVE_NAME, getOrderedUserDefinedOptions(config)));
+
     return new ConfigurationForOutput(
-        skyKey.toString(), configHash, ImmutableList.copyOf(fragments.build()));
+        skyKey.toString(),
+        configHash,
+        fragments.build().asList(),
+        fragmentOptions.build().asList());
   }
 
   /**
@@ -397,7 +503,7 @@
       ConfigCommandOutputFormatter writer,
       ImmutableSortedSet<ConfigurationForOutput> configurations) {
     writer.writeConfigurationIDs(
-        configurations.stream().map(config -> config.configHash).collect(Collectors.toList()));
+        configurations.stream().map(config -> config.configHash).collect(toList()));
     return BlazeCommandResult.exitCode(ExitCode.SUCCESS);
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/ConfigCommandOutputFormatter.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/ConfigCommandOutputFormatter.java
index 836e5c0..56ac467 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/ConfigCommandOutputFormatter.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/ConfigCommandOutputFormatter.java
@@ -18,6 +18,7 @@
 import com.google.devtools.build.lib.runtime.commands.ConfigCommand.ConfigurationForOutput;
 import com.google.devtools.build.lib.runtime.commands.ConfigCommand.FragmentDiffForOutput;
 import com.google.devtools.build.lib.runtime.commands.ConfigCommand.FragmentForOutput;
+import com.google.devtools.build.lib.runtime.commands.ConfigCommand.FragmentOptionsForOutput;
 import com.google.devtools.build.lib.util.Pair;
 import com.google.gson.Gson;
 import java.io.PrintWriter;
@@ -66,8 +67,19 @@
     public void writeConfiguration(ConfigurationForOutput configuration) {
       writer.println("BuildConfiguration " + configuration.configHash + ":");
       writer.println("Skyframe Key: " + configuration.skyKey);
+
+      StringBuilder fragments = new StringBuilder();
       for (FragmentForOutput fragment : configuration.fragments) {
-        writer.println("Fragment " + fragment.name + " {");
+        fragments
+            .append(fragment.name)
+            .append(": [")
+            .append(String.join(",", fragment.fragmentOptions))
+            .append("], ");
+      }
+
+      writer.println("Fragments: " + fragments);
+      for (FragmentOptionsForOutput fragment : configuration.fragmentOptions) {
+        writer.println("FragmentOptions " + fragment.name + " {");
         for (Map.Entry<String, String> optionSetting : fragment.options.entrySet()) {
           writer.printf("  %s: %s\n", optionSetting.getKey(), optionSetting.getValue());
         }
@@ -87,7 +99,7 @@
       writer.printf(
           "Displaying diff between configs %s and %s\n", diff.configHash1, diff.configHash2);
       for (FragmentDiffForOutput fragmentDiff : diff.fragmentsDiff) {
-        writer.println("Fragment " + fragmentDiff.name + " {");
+        writer.println("FragmentOptions " + fragmentDiff.name + " {");
         for (Map.Entry<String, Pair<String, String>> optionDiff :
             fragmentDiff.optionsDiff.entrySet()) {
           writer.printf(
diff --git a/src/test/java/com/google/devtools/build/lib/runtime/commands/ConfigCommandTest.java b/src/test/java/com/google/devtools/build/lib/runtime/commands/ConfigCommandTest.java
index b41ad28..a953f6d 100644
--- a/src/test/java/com/google/devtools/build/lib/runtime/commands/ConfigCommandTest.java
+++ b/src/test/java/com/google/devtools/build/lib/runtime/commands/ConfigCommandTest.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.analysis.config.FragmentOptions;
 import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase;
 import com.google.devtools.build.lib.runtime.BlazeCommandDispatcher;
 import com.google.devtools.build.lib.runtime.commands.ConfigCommand.ConfigurationDiffForOutput;
@@ -100,25 +101,25 @@
   }
 
   /**
-   * Returns the given option's value for the given fragment of the given configuration.
+   * Returns the value of an option under a configuration's {@link FragmentOptions}.
    *
    * <p>Throws {@link NoSuchElementException} if it can't be found.
    */
   private static String getOptionValue(
-      ConfigurationForOutput config, String fragmentName, String optionName) {
+      ConfigurationForOutput config, String fragmentOptions, String optionName) {
     List<String> ans =
-        config.fragments.stream()
-            .filter(fragment -> fragment.name.endsWith(fragmentName))
+        config.fragmentOptions.stream()
+            .filter(fragment -> fragment.name.endsWith(fragmentOptions))
             .flatMap(fragment -> fragment.options.entrySet().stream())
             .filter(setting -> setting.getKey().equals(optionName))
             .map(entry -> entry.getValue())
             .collect(Collectors.toList());
     if (ans.size() > 1) {
       throw new NoSuchElementException(
-          String.format("Multple matches for fragment=%s, option=%s", fragmentName, optionName));
+          String.format("Multple matches for fragment=%s, option=%s", fragmentOptions, optionName));
     } else if (ans.isEmpty()) {
       throw new NoSuchElementException(
-          String.format("No matches for fragment=%s, option=%s", fragmentName, optionName));
+          String.format("No matches for fragment=%s, option=%s", fragmentOptions, optionName));
     }
     return ans.get(0);
   }
@@ -128,6 +129,16 @@
         && !Boolean.parseBoolean(getOptionValue(config, "CoreOptions", "is exec configuration"));
   }
 
+  /** Converts {@code a.b.d} to {@code d}. * */
+  private static String getBaseName(String str) {
+    return str.substring(str.lastIndexOf(".") + 1);
+  }
+
+  /** Converts a list of {@code a.b.d} strings to {@code d} form. * */
+  private static List<String> getBaseNames(List<String> list) {
+    return list.stream().map(entry -> getBaseName(entry)).collect(Collectors.toList());
+  }
+
   @Test
   public void showConfigIds() throws Exception {
     analyzeTarget();
@@ -157,12 +168,22 @@
     // Verify the existence of a couple of expected fragments:
     assertThat(
             config.fragments.stream()
-                .map(fragment -> fragment.name.substring(fragment.name.lastIndexOf(".") + 1))
+                .map(
+                    fragment ->
+                        Pair.of(getBaseName(fragment.name), getBaseNames(fragment.fragmentOptions)))
+                .collect(Collectors.toList()))
+        .containsAtLeast(
+            Pair.of("PlatformConfiguration", ImmutableList.of("PlatformOptions")),
+            Pair.of("TestConfiguration", ImmutableList.of("TestConfiguration$TestOptions")));
+    // Verify the existence of a couple of expected fragment options:
+    assertThat(
+            config.fragmentOptions.stream()
+                .map(fragment -> getBaseName(fragment.name))
                 .collect(Collectors.toList()))
         .containsAtLeast("PlatformOptions", "CoreOptions", "user-defined");
     // Verify the existence of a couple of expected option names:
     assertThat(
-            config.fragments.stream()
+            config.fragmentOptions.stream()
                 .filter(fragment -> fragment.name.endsWith("CoreOptions"))
                 .flatMap(fragment -> fragment.options.keySet().stream())
                 .collect(Collectors.toList()))
@@ -288,7 +309,7 @@
     assertThat(getOptionValue(targetConfig, "user-defined", "--define:a")).isEqualTo("1");
     assertThat(getOptionValue(targetConfig, "user-defined", "--define:b")).isEqualTo("2");
     assertThat(
-            targetConfig.fragments.stream()
+            targetConfig.fragmentOptions.stream()
                 .filter(fragment -> fragment.name.endsWith("CoreOptions"))
                 .flatMap(fragment -> fragment.options.keySet().stream())
                 .filter(name -> name.equals("define"))