Refactor BlazeOptionHandler to pull out some functionality into dedicated classes.

RELNOTES:None
PiperOrigin-RevId: 301893566
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeOptionHandler.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeOptionHandler.java
index 9b5d9ff..4896a96 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeOptionHandler.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeOptionHandler.java
@@ -16,9 +16,7 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Joiner;
 import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.EventHandler;
@@ -26,21 +24,16 @@
 import com.google.devtools.build.lib.runtime.commands.ProjectFileSupport;
 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy;
 import com.google.devtools.build.lib.util.ExitCode;
-import com.google.devtools.build.lib.util.OS;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.common.options.InvocationPolicyEnforcer;
 import com.google.devtools.common.options.OptionDefinition;
 import com.google.devtools.common.options.OptionPriority.PriorityCategory;
-import com.google.devtools.common.options.OptionValueDescription;
 import com.google.devtools.common.options.OptionsParser;
 import com.google.devtools.common.options.OptionsParsingException;
 import com.google.devtools.common.options.OptionsParsingResult;
-import com.google.devtools.common.options.ParsedOptionDescription;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.function.Function;
@@ -168,20 +161,20 @@
     for (String commandToParse : getCommandNamesToParse(commandAnnotation)) {
       // Get all args defined for this command (or "common"), grouped by rc chunk.
       for (RcChunkOfArgs rcArgs : commandToRcArgs.get(commandToParse)) {
-        if (!rcArgs.args.isEmpty()) {
+        if (!rcArgs.getArgs().isEmpty()) {
           String inherited = commandToParse.equals(commandAnnotation.name()) ? "" : "Inherited ";
           String source =
-              rcArgs.rcFile.equals("client")
+              rcArgs.getRcFile().equals("client")
                   ? "Options provided by the client"
                   : String.format(
                       "Reading rc options for '%s' from %s",
-                      commandAnnotation.name(), rcArgs.rcFile);
+                      commandAnnotation.name(), rcArgs.getRcFile());
           rcfileNotes.add(
               String.format(
                   "%s:\n  %s'%s' options: %s",
-                  source, inherited, commandToParse, Joiner.on(' ').join(rcArgs.args)));
+                  source, inherited, commandToParse, Joiner.on(' ').join(rcArgs.getArgs())));
         }
-        optionsParser.parse(PriorityCategory.RC_FILE, rcArgs.rcFile, rcArgs.args);
+        optionsParser.parse(PriorityCategory.RC_FILE, rcArgs.getRcFile(), rcArgs.getArgs());
       }
     }
   }
@@ -316,44 +309,6 @@
     return ExitCode.SUCCESS;
   }
 
-  private static String getPlatformName() {
-    switch (OS.getCurrent()) {
-      case LINUX:
-        return "linux";
-      case DARWIN:
-        return "macos";
-      case WINDOWS:
-        return "windows";
-      case FREEBSD:
-        return "freebsd";
-      case OPENBSD:
-        return "openbsd";
-      default:
-        return OS.getCurrent().getCanonicalName();
-    }
-  }
-
-  /**
-   * If --enable_platform_specific_config is true and the corresponding config definition exists, we
-   * should enable the platform specific config.
-   */
-  private boolean shouldEnablePlatformSpecificConfig(
-      OptionValueDescription enablePlatformSpecificConfigDescription,
-      ListMultimap<String, RcChunkOfArgs> commandToRcArgs) {
-    if (enablePlatformSpecificConfigDescription == null
-        || !(boolean) enablePlatformSpecificConfigDescription.getValue()) {
-      return false;
-    }
-
-    for (String commandName : getCommandNamesToParse(commandAnnotation)) {
-      String defaultConfigDef = commandName + ":" + getPlatformName();
-      if (commandToRcArgs.containsKey(defaultConfigDef)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
   /**
    * Expand the values of --config according to the definitions provided in the rc files and the
    * applicable command.
@@ -361,162 +316,12 @@
   void expandConfigOptions(
       EventHandler eventHandler, ListMultimap<String, RcChunkOfArgs> commandToRcArgs)
       throws OptionsParsingException {
-
-    OptionValueDescription configValueDescription =
-        optionsParser.getOptionValueDescription("config");
-    if (configValueDescription != null && configValueDescription.getCanonicalInstances() != null) {
-      // Find the base set of configs. This does not include the config options that might be
-      // recursively included.
-      ImmutableList<ParsedOptionDescription> configInstances =
-          ImmutableList.copyOf(configValueDescription.getCanonicalInstances());
-
-      // Expand the configs that are mentioned in the input. Flatten these expansions before parsing
-      // them, to preserve order.
-      for (ParsedOptionDescription configInstance : configInstances) {
-        String configValueToExpand = (String) configInstance.getConvertedValue();
-        List<String> expansion = getExpansion(eventHandler, commandToRcArgs, configValueToExpand);
-        optionsParser.parseArgsAsExpansionOfOption(
-            configInstance, String.format("expanded from --%s", configValueToExpand), expansion);
-      }
-    }
-
-    OptionValueDescription enablePlatformSpecificConfigDescription =
-        optionsParser.getOptionValueDescription("enable_platform_specific_config");
-    if (shouldEnablePlatformSpecificConfig(
-        enablePlatformSpecificConfigDescription, commandToRcArgs)) {
-      List<String> expansion = getExpansion(eventHandler, commandToRcArgs, getPlatformName());
-      optionsParser.parseArgsAsExpansionOfOption(
-          Iterables.getOnlyElement(enablePlatformSpecificConfigDescription.getCanonicalInstances()),
-          String.format("enabled by --enable_platform_specific_config"),
-          expansion);
-    }
-
-    // At this point, we've expanded everything, identify duplicates, if any, to warn about
-    // re-application.
-    List<String> configs = optionsParser.getOptions(CommonCommandOptions.class).configs;
-    Set<String> configSet = new HashSet<>();
-    LinkedHashSet<String> duplicateConfigs = new LinkedHashSet<>();
-    for (String configValue : configs) {
-      if (!configSet.add(configValue)) {
-        duplicateConfigs.add(configValue);
-      }
-    }
-    if (!duplicateConfigs.isEmpty()) {
-      eventHandler.handle(
-          Event.warn(
-              String.format(
-                  "The following configs were expanded more than once: %s. For repeatable flags, "
-                      + "repeats are counted twice and may lead to unexpected behavior.",
-                  duplicateConfigs)));
-    }
-  }
-
-  private List<String> getExpansion(
-      EventHandler eventHandler,
-      ListMultimap<String, RcChunkOfArgs> commandToRcArgs,
-      String configToExpand)
-      throws OptionsParsingException {
-    LinkedHashSet<String> configAncestorSet = new LinkedHashSet<>();
-    configAncestorSet.add(configToExpand);
-    List<String> longestChain = new ArrayList<>();
-    List<String> finalExpansion =
-        getExpansion(
-            eventHandler, commandToRcArgs, configAncestorSet, configToExpand, longestChain);
-
-    // In order to prevent warning about a long chain of 13 configs at the 10, 11, 12, and 13
-    // point, we identify the longest chain for this 'high-level' --config found and only warn
-    // about it once. This may mean we missed a fork where each branch was independently long
-    // enough to warn, but the single warning should convey the message reasonably.
-    if (longestChain.size() >= 10) {
-      eventHandler.handle(
-          Event.warn(
-              String.format(
-                  "There is a recursive chain of configs %s configs long: %s. This seems "
-                      + "excessive, and might be hiding errors.",
-                  longestChain.size(), longestChain)));
-    }
-    return finalExpansion;
-  }
-
-  /**
-   * @param configAncestorSet is the chain of configs that have led to this one getting expanded.
-   *     This should only contain the configs that expanded, recursively, to this one, and should
-   *     not contain "siblings," as it is used to detect cycles. {@code build:foo --config=bar},
-   *     {@code build:bar --config=foo}, is a cycle, detected because this list will be [foo, bar]
-   *     when we find another 'foo' to expand. However, {@code build:foo --config=bar}, {@code
-   *     build:foo --config=bar} is not a cycle just because bar is expanded twice, and the 1st bar
-   *     should not be in the parents list of the second bar.
-   * @param longestChain will be populated with the longest inheritance chain of configs.
-   */
-  private List<String> getExpansion(
-      EventHandler eventHandler,
-      ListMultimap<String, RcChunkOfArgs> commandToRcArgs,
-      LinkedHashSet<String> configAncestorSet,
-      String configToExpand,
-      List<String> longestChain)
-      throws OptionsParsingException {
-    List<String> expansion = new ArrayList<>();
-    boolean foundDefinition = false;
-    // The expansion order of rc files is first by command priority, and then in the order the
-    // rc files were read, respecting import statement placement.
-    for (String commandToParse : getCommandNamesToParse(commandAnnotation)) {
-      String configDef = commandToParse + ":" + configToExpand;
-      for (RcChunkOfArgs rcArgs : commandToRcArgs.get(configDef)) {
-        foundDefinition = true;
-        rcfileNotes.add(
-            String.format(
-                "Found applicable config definition %s in file %s: %s",
-                configDef, rcArgs.rcFile, String.join(" ", rcArgs.args)));
-
-        // For each arg in the rcARgs chunk, we first check if it is a config, and if so, expand
-        // it in place. We avoid cycles by tracking the parents of this config.
-        for (String arg : rcArgs.args) {
-          expansion.add(arg);
-          if (arg.length() >= 8 && arg.substring(0, 8).equals("--config")) {
-            // We have a config. For sanity, because we don't want to worry about formatting,
-            // we will only accept --config=value, and will not accept value on a following line.
-            int charOfConfigValue = arg.indexOf('=');
-            if (charOfConfigValue < 0) {
-              throw new OptionsParsingException(
-                  String.format(
-                      "In file %s, the definition of config %s expands to another config "
-                          + "that either has no value or is not in the form --config=value. For "
-                          + "recursive config definitions, please do not provide the value in a "
-                          + "separate token, such as in the form '--config value'.",
-                      rcArgs.rcFile, configToExpand));
-            }
-            String newConfigValue = arg.substring(charOfConfigValue + 1);
-            LinkedHashSet<String> extendedConfigAncestorSet =
-                new LinkedHashSet<>(configAncestorSet);
-            if (!extendedConfigAncestorSet.add(newConfigValue)) {
-              throw new OptionsParsingException(
-                  String.format(
-                      "Config expansion has a cycle: config value %s expands to itself, "
-                          + "see inheritance chain %s",
-                      newConfigValue, extendedConfigAncestorSet));
-            }
-            if (extendedConfigAncestorSet.size() > longestChain.size()) {
-              longestChain.clear();
-              longestChain.addAll(extendedConfigAncestorSet);
-            }
-
-            expansion.addAll(
-                getExpansion(
-                    eventHandler,
-                    commandToRcArgs,
-                    extendedConfigAncestorSet,
-                    newConfigValue,
-                    longestChain));
-          }
-        }
-      }
-    }
-
-    if (!foundDefinition) {
-      throw new OptionsParsingException(
-          "Config value " + configToExpand + " is not defined in any .rc file");
-    }
-    return expansion;
+    ConfigExpander.expandConfigOptions(
+        eventHandler,
+        commandToRcArgs,
+        getCommandNamesToParse(commandAnnotation),
+        rcfileNotes::add,
+        optionsParser);
   }
 
   private static List<String> getCommandNamesToParse(Command commandAnnotation) {
@@ -549,43 +354,6 @@
   }
 
   /**
-   * We receive the rc file arguments from the client in an order that maintains the location of
-   * "import" statements, expanding the imported rc file in place so that its args override previous
-   * args in the file and are overridden by later arguments. We cannot group the args by rc file for
-   * parsing, as we would lose this ordering, so we store them in these "chunks."
-   *
-   * <p>Each chunk comes from a single rc file, but the args stored here may not contain the entire
-   * file if its contents were interrupted by an import statement.
-   */
-  static class RcChunkOfArgs {
-    public RcChunkOfArgs(String rcFile, List<String> args) {
-      this.rcFile = rcFile;
-      this.args = args;
-    }
-
-    // The name of the rc file, usually a path.
-    String rcFile;
-    // The list of arguments specified in this rc "chunk". This is all for a single command (or
-    // command:config definition), as different commands will be grouped together, so this list of
-    // arguments can all be parsed as a continuous group.
-    List<String> args;
-
-    @Override
-    public boolean equals(Object o) {
-      if (o instanceof RcChunkOfArgs) {
-        RcChunkOfArgs other = (RcChunkOfArgs) o;
-        return rcFile.equals(other.rcFile) && args.equals(other.args);
-      }
-      return false;
-    }
-
-    @Override
-    public int hashCode() {
-      return rcFile.hashCode() + args.hashCode();
-    }
-  }
-
-  /**
    * The rc options are passed via {@link ClientOptions#optionsOverrides} and {@link
    * ClientOptions#rcSource}, which is basically a line-by-line transfer of the rc files read by the
    * client. This is not a particularly useful format for expanding the options, so this method
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/ConfigExpander.java b/src/main/java/com/google/devtools/build/lib/runtime/ConfigExpander.java
new file mode 100644
index 0000000..31e86a7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/ConfigExpander.java
@@ -0,0 +1,271 @@
+// 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.build.lib.runtime;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.util.OS;
+import com.google.devtools.common.options.OptionValueDescription;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsParsingException;
+import com.google.devtools.common.options.ParsedOptionDescription;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+
+/** Encapsulates logic for performing --config option expansion. */
+final class ConfigExpander {
+
+  private ConfigExpander() {}
+
+  private static String getPlatformName() {
+    switch (OS.getCurrent()) {
+      case LINUX:
+        return "linux";
+      case DARWIN:
+        return "macos";
+      case WINDOWS:
+        return "windows";
+      case FREEBSD:
+        return "freebsd";
+      case OPENBSD:
+        return "openbsd";
+      default:
+        return OS.getCurrent().getCanonicalName();
+    }
+  }
+
+  /**
+   * If --enable_platform_specific_config is true and the corresponding config definition exists, we
+   * should enable the platform specific config.
+   */
+  private static boolean shouldEnablePlatformSpecificConfig(
+      OptionValueDescription enablePlatformSpecificConfigDescription,
+      ListMultimap<String, RcChunkOfArgs> commandToRcArgs,
+      List<String> commandsToParse) {
+    if (enablePlatformSpecificConfigDescription == null
+        || !(boolean) enablePlatformSpecificConfigDescription.getValue()) {
+      return false;
+    }
+
+    for (String commandName : commandsToParse) {
+      String defaultConfigDef = commandName + ":" + getPlatformName();
+      if (commandToRcArgs.containsKey(defaultConfigDef)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Expands --config options present in the requested commands using the options configuration
+   * provided in commandToRcArgs.
+   *
+   * @param eventHandler collects any warnings encountered.
+   * @param rcFileNotesConsumer collects any informational messages encountered.
+   * @param optionsParser will parse the expanded --config representations.
+   * @throws OptionsParsingException if a fatal problem with the configuration is encountered.
+   */
+  static void expandConfigOptions(
+      EventHandler eventHandler,
+      ListMultimap<String, RcChunkOfArgs> commandToRcArgs,
+      List<String> commandsToParse,
+      Consumer<String> rcFileNotesConsumer,
+      OptionsParser optionsParser)
+      throws OptionsParsingException {
+
+    OptionValueDescription configValueDescription =
+        optionsParser.getOptionValueDescription("config");
+    if (configValueDescription != null && configValueDescription.getCanonicalInstances() != null) {
+      // Find the base set of configs. This does not include the config options that might be
+      // recursively included.
+      ImmutableList<ParsedOptionDescription> configInstances =
+          ImmutableList.copyOf(configValueDescription.getCanonicalInstances());
+
+      // Expand the configs that are mentioned in the input. Flatten these expansions before parsing
+      // them, to preserve order.
+      for (ParsedOptionDescription configInstance : configInstances) {
+        String configValueToExpand = (String) configInstance.getConvertedValue();
+        List<String> expansion =
+            getExpansion(
+                eventHandler,
+                commandToRcArgs,
+                commandsToParse,
+                configValueToExpand,
+                rcFileNotesConsumer);
+        optionsParser.parseArgsAsExpansionOfOption(
+            configInstance, String.format("expanded from --%s", configValueToExpand), expansion);
+      }
+    }
+
+    OptionValueDescription enablePlatformSpecificConfigDescription =
+        optionsParser.getOptionValueDescription("enable_platform_specific_config");
+    if (shouldEnablePlatformSpecificConfig(
+        enablePlatformSpecificConfigDescription, commandToRcArgs, commandsToParse)) {
+      List<String> expansion =
+          getExpansion(
+              eventHandler,
+              commandToRcArgs,
+              commandsToParse,
+              getPlatformName(),
+              rcFileNotesConsumer);
+      optionsParser.parseArgsAsExpansionOfOption(
+          Iterables.getOnlyElement(enablePlatformSpecificConfigDescription.getCanonicalInstances()),
+          String.format("enabled by --enable_platform_specific_config"),
+          expansion);
+    }
+
+    // At this point, we've expanded everything, identify duplicates, if any, to warn about
+    // re-application.
+    List<String> configs = optionsParser.getOptions(CommonCommandOptions.class).configs;
+    Set<String> configSet = new HashSet<>();
+    LinkedHashSet<String> duplicateConfigs = new LinkedHashSet<>();
+    for (String configValue : configs) {
+      if (!configSet.add(configValue)) {
+        duplicateConfigs.add(configValue);
+      }
+    }
+    if (!duplicateConfigs.isEmpty()) {
+      eventHandler.handle(
+          Event.warn(
+              String.format(
+                  "The following configs were expanded more than once: %s. For repeatable flags, "
+                      + "repeats are counted twice and may lead to unexpected behavior.",
+                  duplicateConfigs)));
+    }
+  }
+
+  private static List<String> getExpansion(
+      EventHandler eventHandler,
+      ListMultimap<String, RcChunkOfArgs> commandToRcArgs,
+      List<String> commandsToParse,
+      String configToExpand,
+      Consumer<String> rcFileNotesConsumer)
+      throws OptionsParsingException {
+    LinkedHashSet<String> configAncestorSet = new LinkedHashSet<>();
+    configAncestorSet.add(configToExpand);
+    List<String> longestChain = new ArrayList<>();
+    List<String> finalExpansion =
+        getExpansion(
+            commandToRcArgs,
+            commandsToParse,
+            configAncestorSet,
+            configToExpand,
+            longestChain,
+            rcFileNotesConsumer);
+
+    // In order to prevent warning about a long chain of 13 configs at the 10, 11, 12, and 13
+    // point, we identify the longest chain for this 'high-level' --config found and only warn
+    // about it once. This may mean we missed a fork where each branch was independently long
+    // enough to warn, but the single warning should convey the message reasonably.
+    if (longestChain.size() >= 10) {
+      eventHandler.handle(
+          Event.warn(
+              String.format(
+                  "There is a recursive chain of configs %s configs long: %s. This seems "
+                      + "excessive, and might be hiding errors.",
+                  longestChain.size(), longestChain)));
+    }
+    return finalExpansion;
+  }
+
+  /**
+   * @param configAncestorSet is the chain of configs that have led to this one getting expanded.
+   *     This should only contain the configs that expanded, recursively, to this one, and should
+   *     not contain "siblings," as it is used to detect cycles. {@code build:foo --config=bar},
+   *     {@code build:bar --config=foo}, is a cycle, detected because this list will be [foo, bar]
+   *     when we find another 'foo' to expand. However, {@code build:foo --config=bar}, {@code
+   *     build:foo --config=bar} is not a cycle just because bar is expanded twice, and the 1st bar
+   *     should not be in the parents list of the second bar.
+   * @param longestChain will be populated with the longest inheritance chain of configs.
+   */
+  private static List<String> getExpansion(
+      ListMultimap<String, RcChunkOfArgs> commandToRcArgs,
+      List<String> commandsToParse,
+      LinkedHashSet<String> configAncestorSet,
+      String configToExpand,
+      List<String> longestChain,
+      Consumer<String> rcFileNotesConsumer)
+      throws OptionsParsingException {
+    List<String> expansion = new ArrayList<>();
+    boolean foundDefinition = false;
+    // The expansion order of rc files is first by command priority, and then in the order the
+    // rc files were read, respecting import statement placement.
+    for (String commandToParse : commandsToParse) {
+      String configDef = commandToParse + ":" + configToExpand;
+      for (RcChunkOfArgs rcArgs : commandToRcArgs.get(configDef)) {
+        foundDefinition = true;
+        rcFileNotesConsumer.accept(
+            String.format(
+                "Found applicable config definition %s in file %s: %s",
+                configDef, rcArgs.getRcFile(), String.join(" ", rcArgs.getArgs())));
+
+        // For each arg in the rcARgs chunk, we first check if it is a config, and if so, expand
+        // it in place. We avoid cycles by tracking the parents of this config.
+        for (String arg : rcArgs.getArgs()) {
+          expansion.add(arg);
+          if (arg.length() >= 8 && arg.substring(0, 8).equals("--config")) {
+            // We have a config. For sanity, because we don't want to worry about formatting,
+            // we will only accept --config=value, and will not accept value on a following line.
+            int charOfConfigValue = arg.indexOf('=');
+            if (charOfConfigValue < 0) {
+              throw new OptionsParsingException(
+                  String.format(
+                      "In file %s, the definition of config %s expands to another config "
+                          + "that either has no value or is not in the form --config=value. For "
+                          + "recursive config definitions, please do not provide the value in a "
+                          + "separate token, such as in the form '--config value'.",
+                      rcArgs.getRcFile(), configToExpand));
+            }
+            String newConfigValue = arg.substring(charOfConfigValue + 1);
+            LinkedHashSet<String> extendedConfigAncestorSet =
+                new LinkedHashSet<>(configAncestorSet);
+            if (!extendedConfigAncestorSet.add(newConfigValue)) {
+              throw new OptionsParsingException(
+                  String.format(
+                      "Config expansion has a cycle: config value %s expands to itself, "
+                          + "see inheritance chain %s",
+                      newConfigValue, extendedConfigAncestorSet));
+            }
+            if (extendedConfigAncestorSet.size() > longestChain.size()) {
+              longestChain.clear();
+              longestChain.addAll(extendedConfigAncestorSet);
+            }
+
+            expansion.addAll(
+                getExpansion(
+                    commandToRcArgs,
+                    commandsToParse,
+                    extendedConfigAncestorSet,
+                    newConfigValue,
+                    longestChain,
+                    rcFileNotesConsumer));
+          }
+        }
+      }
+    }
+
+    if (!foundDefinition) {
+      throw new OptionsParsingException(
+          "Config value " + configToExpand + " is not defined in any .rc file");
+    }
+    return expansion;
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/RcChunkOfArgs.java b/src/main/java/com/google/devtools/build/lib/runtime/RcChunkOfArgs.java
new file mode 100644
index 0000000..6e6fadc
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/runtime/RcChunkOfArgs.java
@@ -0,0 +1,63 @@
+// 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.build.lib.runtime;
+
+import java.util.List;
+
+/**
+ * We receive the rc file arguments from the client in an order that maintains the location of
+ * "import" statements, expanding the imported rc file in place so that its args override previous
+ * args in the file and are overridden by later arguments. We cannot group the args by rc file for
+ * parsing, as we would lose this ordering, so we store them in these "chunks."
+ *
+ * <p>Each chunk comes from a single rc file, but the args stored here may not contain the entire
+ * file if its contents were interrupted by an import statement.
+ */
+final class RcChunkOfArgs {
+  public RcChunkOfArgs(String rcFile, List<String> args) {
+    this.rcFile = rcFile;
+    this.args = args;
+  }
+
+  private final String rcFile;
+  private final List<String> args;
+
+  @Override
+  public boolean equals(Object o) {
+    if (o instanceof RcChunkOfArgs) {
+      RcChunkOfArgs other = (RcChunkOfArgs) o;
+      return getRcFile().equals(other.getRcFile()) && getArgs().equals(other.getArgs());
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return getRcFile().hashCode() + getArgs().hashCode();
+  }
+
+  /** The name of the rc file, usually a path. */
+  String getRcFile() {
+    return rcFile;
+  }
+
+  /**
+   * The list of arguments specified in this rc "chunk". This is all for a single command (or
+   * command:config definition), as different commands will be grouped together, so this list of
+   * arguments can all be parsed as a continuous group.
+   */
+  List<String> getArgs() {
+    return args;
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/runtime/BlazeOptionHandlerTest.java b/src/test/java/com/google/devtools/build/lib/runtime/BlazeOptionHandlerTest.java
index f769fe0..e92b29f 100644
--- a/src/test/java/com/google/devtools/build/lib/runtime/BlazeOptionHandlerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/runtime/BlazeOptionHandlerTest.java
@@ -25,7 +25,6 @@
 import com.google.devtools.build.lib.bazel.rules.BazelRulesModule;
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.StoredEventHandler;
-import com.google.devtools.build.lib.runtime.BlazeOptionHandler.RcChunkOfArgs;
 import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy;
 import com.google.devtools.build.lib.testutil.Scratch;
 import com.google.devtools.build.lib.testutil.TestConstants;