Refactor blazerc parsing out of OptionProcessor. This change should be a no-op behavior-wise.

RELNOTES: None
PiperOrigin-RevId: 184843442
diff --git a/src/main/cpp/BUILD b/src/main/cpp/BUILD
index 82b7859..7f85db7 100644
--- a/src/main/cpp/BUILD
+++ b/src/main/cpp/BUILD
@@ -125,6 +125,7 @@
     ],
     deps = [
         ":blaze_util",
+        ":rc_file",
         ":startup_options",
         ":workspace_layout",
         "//src/main/cpp/util",
@@ -164,6 +165,22 @@
     ],
 )
 
+cc_library(
+    name = "rc_file",
+    srcs = ["rc_file.cc"],
+    hdrs = ["rc_file.h"],
+    visibility = [
+        "//src:__pkg__",
+        "//src/test/cpp:__pkg__",
+    ],
+    deps = [
+        ":blaze_util",
+        ":workspace_layout",
+        "//src/main/cpp/util",
+        "//src/main/cpp/util:logging",
+    ],
+)
+
 filegroup(
     name = "srcs",
     srcs = glob(["**"]) + ["//src/main/cpp/util:srcs"],
diff --git a/src/main/cpp/option_processor.cc b/src/main/cpp/option_processor.cc
index 89fcf2b..83a184c 100644
--- a/src/main/cpp/option_processor.cc
+++ b/src/main/cpp/option_processor.cc
@@ -20,6 +20,7 @@
 #include <algorithm>
 #include <cassert>
 #include <set>
+#include <sstream>
 #include <utility>
 
 #include "src/main/cpp/blaze_util.h"
@@ -41,123 +42,7 @@
 using std::vector;
 
 constexpr char WorkspaceLayout::WorkspacePrefix[];
-
-OptionProcessor::RcOption::RcOption(int rcfile_index, const string& option)
-    : rcfile_index_(rcfile_index), option_(option) {
-}
-
-OptionProcessor::RcFile::RcFile(const string& filename, int index)
-    : filename_(filename), index_(index) {
-}
-
-blaze_exit_code::ExitCode OptionProcessor::RcFile::Parse(
-    const string& workspace,
-    const WorkspaceLayout* workspace_layout,
-    vector<RcFile*>* rcfiles,
-    map<string, vector<RcOption> >* rcoptions,
-    string* error) {
-  list<string> initial_import_stack;
-  initial_import_stack.push_back(filename_);
-  return Parse(
-      workspace, filename_, index_, workspace_layout,
-      rcfiles, rcoptions, &initial_import_stack,
-      error);
-}
-
-blaze_exit_code::ExitCode OptionProcessor::RcFile::Parse(
-    const string& workspace,
-    const string& filename_ref,
-    const int index,
-    const WorkspaceLayout* workspace_layout,
-    vector<RcFile*>* rcfiles,
-    map<string, vector<RcOption> >* rcoptions,
-    list<string>* import_stack,
-    string* error) {
-  string filename(filename_ref);  // file
-  BAZEL_LOG(INFO) << "Parsing the RcFile " << filename;
-  string contents;
-  if (!blaze_util::ReadFile(filename, &contents)) {
-    // We checked for file readability before, so this is unexpected.
-    blaze_util::StringPrintf(error,
-        "Unexpected error reading .blazerc file '%s'", filename.c_str());
-    return blaze_exit_code::INTERNAL_ERROR;
-  }
-
-  // A '\' at the end of a line continues the line.
-  blaze_util::Replace("\\\r\n", "", &contents);
-  blaze_util::Replace("\\\n", "", &contents);
-
-  vector<string> lines = blaze_util::Split(contents, '\n');
-  for (string& line : lines) {
-    blaze_util::StripWhitespace(&line);
-
-    // Check for an empty line.
-    if (line.empty()) {
-      continue;
-    }
-
-    vector<string> words;
-
-    // This will treat "#" as a comment, and properly
-    // quote single and double quotes, and treat '\'
-    // as an escape character.
-    // TODO(bazel-team): This function silently ignores
-    // dangling backslash escapes and missing end-quotes.
-    blaze_util::Tokenize(line, '#', &words);
-
-    if (words.empty()) {
-      // Could happen if line starts with "#"
-      continue;
-    }
-
-    string command = words[0];
-
-    if (command == "import") {
-      if (words.size() != 2
-          || (words[1].compare(0, workspace_layout->WorkspacePrefixLength,
-                               workspace_layout->WorkspacePrefix) == 0
-              && !workspace_layout->WorkspaceRelativizeRcFilePath(
-                  workspace, &words[1]))) {
-        blaze_util::StringPrintf(
-            error,
-            "Invalid import declaration in .blazerc file '%s': '%s'"
-            " (are you in your source checkout/WORKSPACE?)",
-            filename.c_str(), line.c_str());
-        return blaze_exit_code::BAD_ARGV;
-      }
-      if (std::find(import_stack->begin(), import_stack->end(), words[1]) !=
-          import_stack->end()) {
-        string loop;
-        for (list<string>::const_iterator imported_rc = import_stack->begin();
-             imported_rc != import_stack->end(); ++imported_rc) {
-          loop += "  " + *imported_rc + "\n";
-        }
-        blaze_util::StringPrintf(error,
-            "Import loop detected:\n%s", loop.c_str());
-        return blaze_exit_code::BAD_ARGV;
-      }
-
-      rcfiles->push_back(new RcFile(words[1], rcfiles->size()));
-      import_stack->push_back(words[1]);
-      blaze_exit_code::ExitCode parse_exit_code =
-        RcFile::Parse(workspace, rcfiles->back()->Filename(),
-                      rcfiles->back()->Index(), workspace_layout,
-                      rcfiles, rcoptions, import_stack, error);
-      if (parse_exit_code != blaze_exit_code::SUCCESS) {
-        return parse_exit_code;
-      }
-      import_stack->pop_back();
-    } else {
-      vector<string>::const_iterator words_it = words.begin();
-      words_it++;  // Advance past command.
-      for (; words_it != words.end(); words_it++) {
-        (*rcoptions)[command].push_back(RcOption(index, *words_it));
-      }
-    }
-  }
-
-  return blaze_exit_code::SUCCESS;
-}
+static std::vector<std::string> GetProcessedEnv();
 
 OptionProcessor::OptionProcessor(
     const WorkspaceLayout* workspace_layout,
@@ -307,6 +192,21 @@
   return result;
 }
 
+blaze_exit_code::ExitCode ParseErrorToExitCode(RcFile::ParseError parse_error) {
+  switch (parse_error) {
+    case RcFile::ParseError::NONE:
+      return blaze_exit_code::SUCCESS;
+    case RcFile::ParseError::UNREADABLE_FILE:
+      // We check readability before parsing, so this is unexpected.
+      return blaze_exit_code::INTERNAL_ERROR;
+    case RcFile::ParseError::INVALID_FORMAT:
+    case RcFile::ParseError::IMPORT_LOOP:
+      return blaze_exit_code::BAD_ARGV;
+    default:
+      return blaze_exit_code::INTERNAL_ERROR;
+  }
+}
+
 }  // namespace internal
 
 // Parses the arguments provided in args using the workspace path and the
@@ -358,13 +258,13 @@
 
   for (const auto& blazerc_path : deduped_blazerc_paths) {
     if (!blazerc_path.empty()) {
-      blazercs_.push_back(new RcFile(blazerc_path, blazercs_.size()));
-      blaze_exit_code::ExitCode parse_exit_code =
-          blazercs_.back()->Parse(workspace, workspace_layout_, &blazercs_,
-                                  &rcoptions_, error);
-      if (parse_exit_code != blaze_exit_code::SUCCESS) {
-        return parse_exit_code;
+      RcFile::ParseError parse_error;
+      auto rcfile = RcFile::Parse(blazerc_path, workspace_layout_, workspace,
+                                  &parse_error, error);
+      if (rcfile == nullptr) {
+        return internal::ParseErrorToExitCode(parse_error);
       }
+      blazercs_.push_back(std::move(rcfile));
     }
   }
 
@@ -374,8 +274,8 @@
     return parse_startup_options_exit_code;
   }
 
-  blazerc_and_env_command_args_ = GetBlazercAndEnvCommandArgs(
-      cwd, blazercs_, rcoptions_);
+  blazerc_and_env_command_args_ =
+      GetBlazercAndEnvCommandArgs(cwd, blazercs_, GetProcessedEnv());
   return blaze_exit_code::SUCCESS;
 }
 
@@ -425,13 +325,12 @@
   // option_sources tracking and for sending to the server.
   std::vector<RcStartupFlag> rcstartup_flags;
 
-  auto iter = rcoptions_.find("startup");
-  if (iter != rcoptions_.end()) {
-    const vector<RcOption>& startup_rcoptions = iter->second;
-    for (const RcOption& option : startup_rcoptions) {
-      rcstartup_flags.push_back(
-          RcStartupFlag(blazercs_[option.rcfile_index()]->Filename(),
-                        option.option()));
+  for (const auto& blazerc : blazercs_) {
+    const auto iter = blazerc->options().find("startup");
+    if (iter == blazerc->options().end()) continue;
+
+    for (const RcOption& option : iter->second) {
+      rcstartup_flags.push_back({*option.source_path, option.option});
     }
   }
 
@@ -495,13 +394,26 @@
 }
 #endif  // defined(COMPILER_MSVC)
 
+static std::vector<std::string> GetProcessedEnv() {
+  std::vector<std::string> processed_env;
+  for (char** env = environ; *env != NULL; env++) {
+    string env_str(*env);
+    if (IsValidEnvName(*env)) {
+      PreprocessEnvString(&env_str);
+      processed_env.push_back(std::move(env_str));
+    }
+  }
+  return processed_env;
+}
+
 // IMPORTANT: The options added here do not come from the user. In order for
 // their source to be correctly tracked, the options must either be passed
 // as --default_override=0, 0 being "client", or must be listed in
 // BlazeOptionHandler.INTERNAL_COMMAND_OPTIONS!
 std::vector<std::string> OptionProcessor::GetBlazercAndEnvCommandArgs(
-    const std::string& cwd, const std::vector<RcFile*>& blazercs,
-    const std::map<std::string, std::vector<RcOption>>& rcoptions) {
+    const std::string& cwd,
+    const std::vector<std::unique_ptr<RcFile>>& blazercs,
+    const std::vector<std::string>& env) {
   // Provide terminal options as coming from the least important rc file.
   std::vector<std::string> result = {
       "--rc_source=client",
@@ -514,31 +426,42 @@
 
   EnsurePythonPathOption(&result);
 
-  // Push the options mapping .blazerc numbers to filenames.
-  for (const RcFile* blazerc : blazercs) {
-    result.push_back("--rc_source=" + blaze::ConvertPath(blazerc->Filename()));
+  // Map .blazerc numbers to filenames. The indexes here start at 1 because #0
+  // is reserved the "client" options created by this function.
+  int cur_index = 1;
+  std::map<std::string, int> rcfile_indexes;
+  for (const auto& blazerc : blazercs) {
+    for (const std::string& source_path : blazerc->sources()) {
+      // Deduplicate the rc_source list because the same file might be included
+      // from multiple places.
+      if (rcfile_indexes.find(source_path) != rcfile_indexes.end()) continue;
+
+      result.push_back("--rc_source=" + blaze::ConvertPath(source_path));
+      rcfile_indexes[source_path] = cur_index;
+      cur_index++;
+    }
   }
 
-  // Process RcOptions except for the startup flags that are already parsed
-  // by the client and shouldn't be included in the command args.
-  for (auto it = rcoptions.begin(); it != rcoptions.end(); ++it) {
-    if (it->first != "startup") {
-      for (const RcOption& rcoption : it->second) {
-        result.push_back(
-            "--default_override=" + ToString(rcoption.rcfile_index() + 1) +
-            ":" + it->first + "=" + rcoption.option());
+  // Add RcOptions as default_overrides.
+  for (const auto& blazerc : blazercs) {
+    for (const auto& command_options : blazerc->options()) {
+      const string& command = command_options.first;
+      // Skip startup flags, which are already parsed by the client.
+      if (command == "startup") continue;
+
+      for (const RcOption& rcoption : command_options.second) {
+        std::ostringstream oss;
+        oss << "--default_override=" << rcfile_indexes[*rcoption.source_path]
+            << ':' << command << '=' << rcoption.option;
+        result.push_back(oss.str());
       }
     }
   }
 
   // Pass the client environment to the server.
-  for (char** env = environ; *env != NULL; env++) {
-    string env_str(*env);
-    if (IsValidEnvName(*env)) {
-      PreprocessEnvString(&env_str);
-      debug_log("--client_env=%s", env_str.c_str());
-      result.push_back("--client_env=" + env_str);
-    }
+  for (const string& env_var : env) {
+    debug_log("--client_env=%s", env_var.c_str());
+    result.push_back("--client_env=" + env_var);
   }
   result.push_back("--client_cwd=" + blaze::ConvertPath(cwd));
   return result;
@@ -574,10 +497,4 @@
   return parsed_startup_options_.get();
 }
 
-OptionProcessor::~OptionProcessor() {
-  for (auto it : blazercs_) {
-    delete it;
-  }
-}
-
 }  // namespace blaze
diff --git a/src/main/cpp/option_processor.h b/src/main/cpp/option_processor.h
index 0fe45ba..d205301 100644
--- a/src/main/cpp/option_processor.h
+++ b/src/main/cpp/option_processor.h
@@ -21,6 +21,7 @@
 #include <string>
 #include <vector>
 
+#include "src/main/cpp/rc_file.h"
 #include "src/main/cpp/startup_options.h"
 #include "src/main/cpp/util/exit_code.h"
 
@@ -56,7 +57,7 @@
   OptionProcessor(const WorkspaceLayout* workspace_layout,
                   std::unique_ptr<StartupOptions> default_startup_options);
 
-  virtual ~OptionProcessor();
+  virtual ~OptionProcessor() {}
 
   // Splits the arguments of a command line invocation.
   //
@@ -113,51 +114,17 @@
   // to the failure. Otherwise, the server will handle any required logging.
   void PrintStartupOptionsProvenanceMessage() const;
 
- private:
-  class RcOption {
-   public:
-    RcOption(int rcfile_index, const std::string& option);
-
-    const int rcfile_index() const { return rcfile_index_; }
-    const std::string& option() const { return option_; }
-
-   private:
-    int rcfile_index_;
-    std::string option_;
-  };
-
-  class RcFile {
-   public:
-    RcFile(const std::string& filename, int index);
-    blaze_exit_code::ExitCode Parse(
-        const std::string& workspace, const WorkspaceLayout* workspace_layout,
-        std::vector<RcFile*>* rcfiles,
-        std::map<std::string, std::vector<RcOption>>* rcoptions,
-        std::string* error);
-    const std::string& Filename() const { return filename_; }
-    const int Index() const { return index_; }
-
-   private:
-    static blaze_exit_code::ExitCode Parse(
-        const std::string& workspace, const std::string& filename,
-        const int index, const WorkspaceLayout* workspace_layout,
-        std::vector<RcFile*>* rcfiles,
-        std::map<std::string, std::vector<RcOption>>* rcoptions,
-        std::list<std::string>* import_stack, std::string* error);
-
-    std::string filename_;
-    int index_;
-  };
-
   blaze_exit_code::ExitCode ParseStartupOptions(std::string* error);
 
+  // Constructs all synthetic command args that should be passed to the
+  // server to configure blazerc options and client environment.
   static std::vector<std::string> GetBlazercAndEnvCommandArgs(
       const std::string& cwd,
-      const std::vector<RcFile*>& blazercs,
-      const std::map<std::string, std::vector<RcOption>>& rcoptions);
+      const std::vector<std::unique_ptr<RcFile>>& blazercs,
+      const std::vector<std::string>& env);
 
   // The list of parsed rc files, this field is initialized by ParseOptions.
-  std::vector<RcFile*> blazercs_;
+  std::vector<std::unique_ptr<RcFile>> blazercs_;
 
   // A map representing the flags parsed from the bazelrc files.
   // A key is a command (e.g. 'build', 'startup') and its value is an ordered
diff --git a/src/main/cpp/rc_file.cc b/src/main/cpp/rc_file.cc
new file mode 100644
index 0000000..bacfd5c
--- /dev/null
+++ b/src/main/cpp/rc_file.cc
@@ -0,0 +1,136 @@
+// Copyright 2018 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.
+
+#include "src/main/cpp/rc_file.h"
+
+#include <algorithm>
+#include <utility>
+
+#include "src/main/cpp/blaze_util.h"
+#include "src/main/cpp/blaze_util_platform.h"
+#include "src/main/cpp/util/file.h"
+#include "src/main/cpp/util/logging.h"
+#include "src/main/cpp/util/strings.h"
+#include "src/main/cpp/workspace_layout.h"
+
+namespace blaze {
+
+using std::deque;
+using std::string;
+using std::vector;
+
+RcFile::RcFile(string filename, const WorkspaceLayout* workspace_layout,
+               string workspace)
+    : filename_(std::move(filename)),
+      workspace_layout_(workspace_layout),
+      workspace_(std::move(workspace)) {}
+
+/*static*/ std::unique_ptr<RcFile> RcFile::Parse(
+    std::string filename, const WorkspaceLayout* workspace_layout,
+    std::string workspace, ParseError* error, std::string* error_text) {
+  std::unique_ptr<RcFile> rcfile(new RcFile(
+      std::move(filename), workspace_layout, std::move(workspace)));
+  deque<string> initial_import_stack = {rcfile->filename_};
+  *error = rcfile->ParseFile(
+      rcfile->filename_, &initial_import_stack, error_text);
+  return (*error == ParseError::NONE) ? std::move(rcfile) : nullptr;
+}
+
+RcFile::ParseError RcFile::ParseFile(const string& filename,
+                                     deque<string>* import_stack,
+                                     string* error_text) {
+  BAZEL_LOG(INFO) << "Parsing the RcFile " << filename;
+  string contents;
+  if (!blaze_util::ReadFile(filename, &contents)) {
+    blaze_util::StringPrintf(error_text,
+        "Unexpected error reading .blazerc file '%s'", filename.c_str());
+    return ParseError::UNREADABLE_FILE;
+  }
+
+  rcfile_paths_.push_back(filename);
+  // Keep a pointer to the filename string in rcfile_paths_ for the RcOptions.
+  string* filename_ptr = &rcfile_paths_.back();
+
+  // A '\' at the end of a line continues the line.
+  blaze_util::Replace("\\\r\n", "", &contents);
+  blaze_util::Replace("\\\n", "", &contents);
+
+  vector<string> lines = blaze_util::Split(contents, '\n');
+  for (string& line : lines) {
+    blaze_util::StripWhitespace(&line);
+
+    // Check for an empty line.
+    if (line.empty()) {
+      continue;
+    }
+
+    vector<string> words;
+
+    // This will treat "#" as a comment, and properly
+    // quote single and double quotes, and treat '\'
+    // as an escape character.
+    // TODO(bazel-team): This function silently ignores
+    // dangling backslash escapes and missing end-quotes.
+    blaze_util::Tokenize(line, '#', &words);
+
+    if (words.empty()) {
+      // Could happen if line starts with "#"
+      continue;
+    }
+
+    string command = words[0];
+
+    if (command == "import") {
+      if (words.size() != 2 ||
+          (words[1].compare(0, workspace_layout_->WorkspacePrefixLength,
+                            workspace_layout_->WorkspacePrefix) == 0 &&
+           !workspace_layout_->WorkspaceRelativizeRcFilePath(workspace_,
+                                                             &words[1]))) {
+        blaze_util::StringPrintf(
+            error_text,
+            "Invalid import declaration in .blazerc file '%s': '%s'"
+            " (are you in your source checkout/WORKSPACE?)",
+            filename.c_str(), line.c_str());
+        return ParseError::INVALID_FORMAT;
+      }
+      if (std::find(import_stack->begin(), import_stack->end(), words[1]) !=
+          import_stack->end()) {
+        string loop;
+        for (const string& imported_rc : *import_stack) {
+          loop += "  " + imported_rc + "\n";
+        }
+        blaze_util::StringPrintf(error_text,
+            "Import loop detected:\n%s", loop.c_str());
+        return ParseError::IMPORT_LOOP;
+      }
+
+      import_stack->push_back(words[1]);
+      ParseError parse_error = ParseFile(words[1], import_stack, error_text);
+      if (parse_error != ParseError::NONE) {
+        return parse_error;
+      }
+      import_stack->pop_back();
+    } else {
+      auto words_it = words.begin();
+      words_it++;  // Advance past command.
+      for (; words_it != words.end(); words_it++) {
+        options_[command].push_back({filename_ptr, *words_it});
+      }
+    }
+  }
+
+  return ParseError::NONE;
+}
+
+}  // namespace blaze
diff --git a/src/main/cpp/rc_file.h b/src/main/cpp/rc_file.h
new file mode 100644
index 0000000..0d462dc
--- /dev/null
+++ b/src/main/cpp/rc_file.h
@@ -0,0 +1,80 @@
+// Copyright 2018 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.
+#ifndef BAZEL_SRC_MAIN_CPP_RC_FILE_H_
+#define BAZEL_SRC_MAIN_CPP_RC_FILE_H_
+
+#include <deque>
+#include <memory>
+#include <unordered_map>
+#include <string>
+#include <vector>
+
+#include "src/main/cpp/workspace_layout.h"
+
+namespace blaze {
+
+// Single option in an rc file.
+struct RcOption {
+  // Keep a pointer to the path string to avoid copying it over and over.
+  std::string* source_path;
+  std::string option;
+};
+
+// Reads and parses a single rc file with all its imports.
+class RcFile {
+ public:
+  // Constructs a parsed rc file object, or returns a nullptr and sets the
+  // error and error text on failure.
+  enum class ParseError { NONE, UNREADABLE_FILE, INVALID_FORMAT, IMPORT_LOOP };
+  static std::unique_ptr<RcFile> Parse(
+      std::string filename, const WorkspaceLayout* workspace_layout,
+      std::string workspace, ParseError* error, std::string* error_text);
+
+  // Returns all relevant rc sources for this file (including itself).
+  const std::deque<std::string>& sources() const { return rcfile_paths_; }
+
+  // Command -> all options for that command (in order of appearance).
+  using OptionMap = std::unordered_map<std::string, std::vector<RcOption>>;
+  const OptionMap& options() const { return options_; }
+
+ private:
+  RcFile(std::string filename, const WorkspaceLayout* workspace_layout,
+         std::string workspace);
+  // Don't allow copying or moving because it can be tricky with the RcOption
+  // string pointers.
+  RcFile(const RcFile&) = delete;
+  RcFile& operator=(const RcFile&) = delete;
+
+  // Recursive call to parse a file and its imports.
+  ParseError ParseFile(const std::string& filename,
+                       std::deque<std::string>* import_stack,
+                       std::string* error_text);
+
+  const std::string filename_;
+
+  // Workspace definition.
+  const WorkspaceLayout* const workspace_layout_;
+  const std::string workspace_;
+
+  // Full closure of rcfile paths imported from this file (including itself).
+  // The RcOption structs point to the strings in here so they need to be stored
+  // in a container that offers stable pointers, like a deque (and not vector).
+  std::deque<std::string> rcfile_paths_;
+  // All options parsed from the file.
+  OptionMap options_;
+};
+
+}  // namespace blaze
+
+#endif  // BAZEL_SRC_MAIN_CPP_RC_FILE_H_