Add export compile commands .gn setting.

Adds a "export_compile_commands" setting to the .gn file. This can be
used as a consistent way to generate a compilation database for projects
that require this (the existing --export-compile-commands switch is
intended for end-users to use with editor integration).

The new variable uses label patterns for better consistency with the
rest of the system. The switch value takes precendence if present.

Bug: 111

Change-Id: Ieec41f55e9a140066e8b3e3f69115ab087f52172
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/14520
Commit-Queue: Brett Wilson <brettw@chromium.org>
Reviewed-by: Takuto Ikuta <tikuta@google.com>
diff --git a/src/gn/command_gen.cc b/src/gn/command_gen.cc
index e212fe9..e6ea5d7 100644
--- a/src/gn/command_gen.cc
+++ b/src/gn/command_gen.cc
@@ -351,26 +351,49 @@
   return res;
 }
 
-bool RunCompileCommandsWriter(const BuildSettings* build_settings,
-                              const Builder& builder,
-                              Err* err) {
+bool RunCompileCommandsWriter(Setup& setup, Err* err) {
+  // The compilation database is written if either the .gn setting is set or if
+  // the command line flag is set. The command line flag takes precedence.
   const base::CommandLine* command_line =
       base::CommandLine::ForCurrentProcess();
+  bool has_switch = command_line->HasSwitch(kSwitchExportCompileCommands);
+
+  bool has_patterns = !setup.export_compile_commands().empty();
+  if (!has_switch && !has_patterns)
+    return true;  // No compilation database needs to be written.
+
   bool quiet = command_line->HasSwitch(switches::kQuiet);
   base::ElapsedTimer timer;
 
-  std::string file_name = "compile_commands.json";
-  std::string target_filters =
-      command_line->GetSwitchValueASCII(kSwitchExportCompileCommands);
+  // The compilation database file goes in the build directory.
+  SourceFile output_file =
+      setup.build_settings().build_dir().ResolveRelativeFile(
+          Value(nullptr, "compile_commands.json"), err);
+  if (output_file.is_null())
+    return false;
+  base::FilePath output_path = setup.build_settings().GetFullPath(output_file);
 
-  bool res = CompileCommandsWriter::RunAndWriteFiles(
-      build_settings, builder, file_name, target_filters, quiet, err);
-  if (res && !quiet) {
+  bool ok = true;
+  if (has_switch) {
+    // Legacy format using the command-line switch.
+    std::string target_filters =
+        command_line->GetSwitchValueASCII(kSwitchExportCompileCommands);
+    ok = CompileCommandsWriter::RunAndWriteFilesLegacyFilters(
+        &setup.build_settings(), setup.builder(), output_path, target_filters,
+        err);
+  } else {
+    // Use the patterns from the .gn file.
+    ok = CompileCommandsWriter::RunAndWriteFiles(
+        &setup.build_settings(), setup.builder(), output_path,
+        setup.export_compile_commands(), err);
+  }
+
+  if (ok && !quiet) {
     OutputString("Generating compile_commands took " +
                  base::Int64ToString(timer.Elapsed().InMilliseconds()) +
                  "ms\n");
   }
-  return res;
+  return ok;
 }
 
 bool RunNinjaPostProcessTools(const BuildSettings* build_settings,
@@ -610,20 +633,17 @@
       This is an unstable format and likely to change without warning.
 
   --export-compile-commands[=<target_name1,target_name2...>]
-      Produces a compile_commands.json file in the root of the build directory
-      containing an array of “command objects”, where each command object
-      specifies one way a translation unit is compiled in the project. If a list
-      of target_name is supplied, only targets that are reachable from any
-      target in any build file whose name is target_name will be used for
-      “command objects” generation, otherwise all available targets will be used.
-      This is used for various Clang-based tooling, allowing for the replay of
-      individual compilations independent of the build system.
-      e.g. "foo" will match:
-      - "//path/to/src:foo"
-      - "//other/path:foo"
-      - "//foo:foo"
+      Overrides the value of the export_compile_commands in the .gn file (see
+      "gn help dotfile").
+
+      Unlike the .gn setting, this switch takes a legacy format which is a list
+      of target names that are matched in any directory. For example, "foo" will
+      match:
+       - "//path/to/src:foo"
+       - "//other/path:foo"
+       - "//foo:foo"
       and not match:
-      - "//foo:bar"
+       - "//foo:bar"
 )";
 
 int RunGen(const std::vector<std::string>& args) {
@@ -718,9 +738,7 @@
     return 1;
   }
 
-  if (command_line->HasSwitch(kSwitchExportCompileCommands) &&
-      !RunCompileCommandsWriter(&setup->build_settings(), setup->builder(),
-                                &err)) {
+  if (!RunCompileCommandsWriter(*setup, &err)) {
     err.PrintToStdout();
     return 1;
   }
diff --git a/src/gn/compile_commands_writer.cc b/src/gn/compile_commands_writer.cc
index a2ee3a0..10c5bf5 100644
--- a/src/gn/compile_commands_writer.cc
+++ b/src/gn/compile_commands_writer.cc
@@ -300,17 +300,25 @@
 bool CompileCommandsWriter::RunAndWriteFiles(
     const BuildSettings* build_settings,
     const Builder& builder,
-    const std::string& file_name,
-    const std::string& target_filters,
-    bool quiet,
+    const base::FilePath& output_path,
+    const std::vector<LabelPattern>& patterns,
     Err* err) {
-  SourceFile output_file = build_settings->build_dir().ResolveRelativeFile(
-      Value(nullptr, file_name), err);
-  if (output_file.is_null())
-    return false;
+  std::vector<const Target*> to_write =
+      CollectDepsOfMatches(builder.GetAllResolvedTargets(), patterns);
 
-  base::FilePath output_path = build_settings->GetFullPath(output_file);
+  StringOutputBuffer json;
+  std::ostream output_to_json(&json);
+  OutputJSON(build_settings, to_write, output_to_json);
 
+  return json.WriteToFileIfChanged(output_path, err);
+}
+
+bool CompileCommandsWriter::RunAndWriteFilesLegacyFilters(
+    const BuildSettings* build_settings,
+    const Builder& builder,
+    const base::FilePath& output_path,
+    const std::string& target_filters,
+    Err* err) {
   std::vector<const Target*> all_targets = builder.GetAllResolvedTargets();
 
   std::set<std::string> target_filters_set;
@@ -333,6 +341,59 @@
   return json.WriteToFileIfChanged(output_path, err);
 }
 
+std::vector<const Target*> CompileCommandsWriter::CollectDepsOfMatches(
+    const std::vector<const Target*>& all_targets,
+    const std::vector<LabelPattern>& patterns) {
+  // The current set of matched targets.
+  TargetSet collected;
+
+  // Represents the next layer of the breadth-first seach. These are all targets
+  // that we haven't checked so far.
+  std::vector<const Target*> frontier;
+
+  // Collect the first level of target matches. These are the ones that the
+  // patterns match directly.
+  for (const Target* target : all_targets) {
+    if (LabelPattern::VectorMatches(patterns, target->label())) {
+      collected.add(target);
+      frontier.push_back(target);
+    }
+  }
+
+  // Collects the dependencies for the next level of iteration. This could be
+  // inside the loop but is kept outside to avoid reallocating in every
+  // iteration.
+  std::vector<const Target*> next_frontier;
+
+  // Loop for each level of the search.
+  while (!frontier.empty()) {
+    for (const Target* target : frontier) {
+      // Check the target's dependencies.
+      for (const auto& pair : target->GetDeps(Target::DEPS_ALL)) {
+        if (!collected.contains(pair.ptr)) {
+          // New dependency found.
+          collected.add(pair.ptr);
+          next_frontier.push_back(pair.ptr);
+        }
+      }
+    }
+
+    // Swap to the new level and clear out the next one without deallocating the
+    // buffer (in most STL implementations, clear() doesn't free the existing
+    // buffer).
+    std::swap(frontier, next_frontier);
+    next_frontier.clear();
+  }
+
+  // Convert to vector for output.
+  std::vector<const Target*> output;
+  output.reserve(collected.size());
+  for (const Target* target : collected) {
+    output.push_back(target);
+  }
+  return output;
+}
+
 std::vector<const Target*> CompileCommandsWriter::FilterTargets(
     const std::vector<const Target*>& all_targets,
     const std::set<std::string>& target_filters_set) {
diff --git a/src/gn/compile_commands_writer.h b/src/gn/compile_commands_writer.h
index 03006c8..3193f50 100644
--- a/src/gn/compile_commands_writer.h
+++ b/src/gn/compile_commands_writer.h
@@ -5,7 +5,10 @@
 #ifndef TOOLS_GN_COMPILE_COMMANDS_WRITER_H_
 #define TOOLS_GN_COMPILE_COMMANDS_WRITER_H_
 
+#include <vector>
+
 #include "gn/err.h"
+#include "gn/label_pattern.h"
 #include "gn/target.h"
 
 class Builder;
@@ -13,23 +16,39 @@
 
 class CompileCommandsWriter {
  public:
-  // Write compile commands into a json file located by parameter file_name.
-  //
-  // Parameter target_filters should be in "target_name1,target_name2..."
-  // format. If it is not empty, only targets that are reachable from targets
-  // in target_filters are used to generate compile commands.
-  //
-  // Parameter quiet is not used.
+  // Writes a compilation database to the given file name consisting of the
+  // recursive dependencies of all targets that match or are dependencies of
+  // targets that match any given pattern.
   static bool RunAndWriteFiles(const BuildSettings* build_setting,
                                const Builder& builder,
-                               const std::string& file_name,
-                               const std::string& target_filters,
-                               bool quiet,
+                               const base::FilePath& output_path,
+                               const std::vector<LabelPattern>& patterns,
                                Err* err);
 
+  // Writes a compilation database using the legacy way of specifying which
+  // targets to output. This format uses a comma-separated list of target names
+  // ("target_name1,target_name2...") which are matched against targets in any
+  // directory. Then the recursive dependencies of these deps are collected.
+  //
+  // TODO: Remove this legacy target_name behavior and use vector<LabelPattern>
+  // version consistently.
+  static bool RunAndWriteFilesLegacyFilters(const BuildSettings* build_setting,
+                                            const Builder& builder,
+                                            const base::FilePath& output_path,
+                                            const std::string& target_filters,
+                                            Err* err);
+
   static std::string RenderJSON(const BuildSettings* build_settings,
                                 std::vector<const Target*>& all_targets);
 
+  // Does a depth-first search of the graph starting at each target that matches
+  // the given pattern, and collects all recursive dependencies of those
+  // targets.
+  static std::vector<const Target*> CollectDepsOfMatches(
+      const std::vector<const Target*>& all_targets,
+      const std::vector<LabelPattern>& patterns);
+
+  // Performs the legacy target_name filtering.
   static std::vector<const Target*> FilterTargets(
       const std::vector<const Target*>& all_targets,
       const std::set<std::string>& target_filters_set);
diff --git a/src/gn/compile_commands_writer_unittest.cc b/src/gn/compile_commands_writer_unittest.cc
index 29e6a57..f2582c5 100644
--- a/src/gn/compile_commands_writer_unittest.cc
+++ b/src/gn/compile_commands_writer_unittest.cc
@@ -586,6 +586,102 @@
   EXPECT_EQ(expected, out);
 }
 
+TEST_F(CompileCommandsTest, CollectDepsOfMatches) {
+  // Contruct the dependency tree:
+  //
+  //   //foo:bar1
+  //     //base:base
+  //   //foo:bar2
+  //     //base:i18n
+  //       //base
+  //       //third_party:icu
+  //   //random:random
+  Err err;
+  std::vector<const Target*> targets;
+
+  Target icu_target(settings(), Label(SourceDir("//third_party/"), "icu"));
+  icu_target.set_output_type(Target::SOURCE_SET);
+  icu_target.visibility().SetPublic();
+  icu_target.SetToolchain(toolchain());
+  ASSERT_TRUE(icu_target.OnResolved(&err));
+  targets.push_back(&icu_target);
+
+  Target base_target(settings(), Label(SourceDir("//base/"), "base"));
+  base_target.set_output_type(Target::SOURCE_SET);
+  base_target.visibility().SetPublic();
+  base_target.SetToolchain(toolchain());
+  ASSERT_TRUE(base_target.OnResolved(&err));
+  targets.push_back(&base_target);
+
+  Target base_i18n(settings(), Label(SourceDir("//base/"), "i18n"));
+  base_i18n.set_output_type(Target::SOURCE_SET);
+  base_i18n.visibility().SetPublic();
+  base_i18n.private_deps().push_back(LabelTargetPair(&icu_target));
+  base_i18n.public_deps().push_back(LabelTargetPair(&base_target));
+  base_i18n.SetToolchain(toolchain());
+  ASSERT_TRUE(base_i18n.OnResolved(&err))
+      << err.message() << " " << err.help_text();
+  targets.push_back(&base_i18n);
+
+  Target target1(settings(), Label(SourceDir("//foo/"), "bar1"));
+  target1.set_output_type(Target::SOURCE_SET);
+  target1.public_deps().push_back(LabelTargetPair(&base_target));
+  target1.SetToolchain(toolchain());
+  ASSERT_TRUE(target1.OnResolved(&err));
+  targets.push_back(&target1);
+
+  Target target2(settings(), Label(SourceDir("//foo/"), "bar2"));
+  target2.set_output_type(Target::SOURCE_SET);
+  target2.public_deps().push_back(LabelTargetPair(&base_i18n));
+  target2.SetToolchain(toolchain());
+  ASSERT_TRUE(target2.OnResolved(&err));
+  targets.push_back(&target2);
+
+  Target random_target(settings(), Label(SourceDir("//random/"), "random"));
+  random_target.set_output_type(Target::SOURCE_SET);
+  random_target.SetToolchain(toolchain());
+  ASSERT_TRUE(random_target.OnResolved(&err));
+  targets.push_back(&random_target);
+
+  // Sort for stability in comparisons below.
+  auto compare_label = [](const Target* a, const Target* b) -> bool {
+    return a->label() < b->label();
+  };
+  std::sort(targets.begin(), targets.end(), compare_label);
+
+  // Collect everything, the result should match the input.
+  const std::string source_root("/home/me/build/");
+  LabelPattern wildcard_pattern = LabelPattern::GetPattern(
+      SourceDir(), source_root, Value(nullptr, "//*"), &err);
+  ASSERT_FALSE(err.has_error());
+  std::vector<const Target*> output =
+      CompileCommandsWriter::CollectDepsOfMatches(
+          targets, std::vector<LabelPattern>{wildcard_pattern});
+  std::sort(output.begin(), output.end(), compare_label);
+  EXPECT_EQ(output, targets);
+
+  // Collect nothing.
+  output = CompileCommandsWriter::CollectDepsOfMatches(
+      targets, std::vector<LabelPattern>());
+  EXPECT_TRUE(output.empty());
+
+  // Collect all deps of "//foo/*".
+  LabelPattern foo_wildcard = LabelPattern::GetPattern(
+      SourceDir(), source_root, Value(nullptr, "//foo/*"), &err);
+  ASSERT_FALSE(err.has_error());
+  output = CompileCommandsWriter::CollectDepsOfMatches(
+      targets, std::vector<LabelPattern>{foo_wildcard});
+
+  // The result should be everything except "random".
+  std::sort(output.begin(), output.end(), compare_label);
+  ASSERT_EQ(5u, output.size());
+  EXPECT_EQ(&base_target, output[0]);  // Note: order sorted by label.
+  EXPECT_EQ(&base_i18n, output[1]);
+  EXPECT_EQ(&target1, output[2]);
+  EXPECT_EQ(&target2, output[3]);
+  EXPECT_EQ(&icu_target, output[4]);
+}
+
 TEST_F(CompileCommandsTest, CompDBFilter) {
   Err err;
 
diff --git a/src/gn/setup.cc b/src/gn/setup.cc
index 68d42b7..41bd96d 100644
--- a/src/gn/setup.cc
+++ b/src/gn/setup.cc
@@ -23,6 +23,7 @@
 #include "gn/exec_process.h"
 #include "gn/filesystem_utils.h"
 #include "gn/input_file.h"
+#include "gn/label_pattern.h"
 #include "gn/parse_tree.h"
 #include "gn/parser.h"
 #include "gn/source_dir.h"
@@ -116,6 +117,25 @@
           "//build/my_config.gni",
         ]
 
+  export_compile_commands [optional]
+      A list of label patterns for which to generate a Clang compilation
+      database (see "gn help label_pattern" for the string format).
+
+      When specified, GN will generate a compile_commands.json file in the root
+      of the build directory containing information on how to compile each
+      source file reachable from any label matching any pattern in the list.
+      This is used for Clang-based tooling and some editor integration. See
+      https://clang.llvm.org/docs/JSONCompilationDatabase.html
+
+      The switch --export-compile-commands to "gn gen" (see "gn help gen")
+      overwrites this value.
+
+      Example:
+        export_compile_commands = [
+          "//base/*",
+          "//tools:doom_melon",
+        ]
+
   root [optional]
       Label of the root build target. The GN build will start by loading the
       build file containing this target name. This defaults to "//:" which will
@@ -1045,7 +1065,19 @@
       return false;
     }
     build_settings_.set_no_stamp_files(no_stamp_files_value->boolean_value());
-    CHECK(!build_settings_.no_stamp_files()) << "no_stamp_files does not work yet!";
+    CHECK(!build_settings_.no_stamp_files())
+        << "no_stamp_files does not work yet!";
+  }
+
+  // Export compile commands.
+  const Value* export_cc_value =
+      dotfile_scope_.GetValue("export_compile_commands", true);
+  if (export_cc_value) {
+    if (!ExtractListOfLabelPatterns(&build_settings_, *export_cc_value,
+                                    SourceDir("//"), &export_compile_commands_,
+                                    err)) {
+      return false;
+    }
   }
 
   return true;
diff --git a/src/gn/setup.h b/src/gn/setup.h
index 2809a7d..91aca82 100644
--- a/src/gn/setup.h
+++ b/src/gn/setup.h
@@ -113,6 +113,10 @@
     return no_check_patterns_.get();
   }
 
+  const std::vector<LabelPattern>& export_compile_commands() const {
+    return export_compile_commands_;
+  }
+
   BuildSettings& build_settings() { return build_settings_; }
   Builder& builder() { return builder_; }
   LoaderImpl* loader() { return loader_.get(); }
@@ -207,6 +211,8 @@
   std::vector<Token> args_tokens_;
   std::unique_ptr<ParseNode> args_root_;
 
+  std::vector<LabelPattern> export_compile_commands_;
+
   Setup(const Setup&) = delete;
   Setup& operator=(const Setup&) = delete;
 };