[rust-project] support proc macros

Rust IDEs (such as VSCode) nowadays have excellent support for expanding
procedural macros, so long as they're fed the right information. gn's
output of rust-project.json was lacking a couple of fields.

Bug: chromium/1293933
Change-Id: I842ba5ad7a19f95a785d7c9e1517203bf9e88bb7
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/13340
Reviewed-by: Tyler Mandry <tmandry@google.com>
Reviewed-by: Brett Wilson <brettw@chromium.org>
Commit-Queue: Brett Wilson <brettw@chromium.org>
diff --git a/src/gn/rust_project_writer.cc b/src/gn/rust_project_writer.cc
index c11f967..4e66dcc 100644
--- a/src/gn/rust_project_writer.cc
+++ b/src/gn/rust_project_writer.cc
@@ -320,6 +320,15 @@
     }
   }
 
+  // If it's a proc macro, record its output location so IDEs can invoke it.
+  if (std::string_view(rust_tool->name()) ==
+      std::string_view(RustTool::kRsToolMacro)) {
+    auto outputs = target->computed_outputs();
+    if (outputs.size() > 0) {
+      crate.SetIsProcMacro(outputs[0]);
+    }
+  }
+
   // Add the rest of the crate dependencies.
   for (const auto& dep : crate_deps) {
     auto idx = lookup[dep];
@@ -404,6 +413,15 @@
 
     rust_project << "      \"edition\": \"" << crate.edition() << "\"," NEWLINE;
 
+    auto proc_macro_target = crate.proc_macro_path();
+    if (proc_macro_target.has_value()) {
+      rust_project << "      \"is_proc_macro\": true," NEWLINE;
+      auto so_location = FilePathToUTF8(build_settings->GetFullPath(
+          proc_macro_target->AsSourceFile(build_settings)));
+      rust_project << "      \"proc_macro_dylib_path\": \"" << so_location
+                   << "\"," NEWLINE;
+    }
+
     rust_project << "      \"cfg\": [";
     bool first_cfg = true;
     for (const auto& cfg : crate.configs()) {
diff --git a/src/gn/rust_project_writer_helpers.h b/src/gn/rust_project_writer_helpers.h
index a63ded1..d53ff07 100644
--- a/src/gn/rust_project_writer_helpers.h
+++ b/src/gn/rust_project_writer_helpers.h
@@ -55,6 +55,11 @@
   // Set the compiler target ("e.g. x86_64-linux-kernel")
   void SetCompilerTarget(std::string target) { compiler_target_ = target; }
 
+  // Set that this is a proc macro with the path to the output .so/dylib/dll
+  void SetIsProcMacro(OutputFile proc_macro_dynamic_library) {
+    proc_macro_dynamic_library_ = proc_macro_dynamic_library;
+  }
+
   // Returns the root file for the crate.
   SourceFile& root() { return root_; }
 
@@ -81,6 +86,11 @@
     return compiler_target_;
   }
 
+  // Returns whether this crate builds a proc macro .so
+  const std::optional<OutputFile>& proc_macro_path() {
+    return proc_macro_dynamic_library_;
+  }
+
  private:
   SourceFile root_;
   CrateIndex index_;
@@ -90,6 +100,7 @@
   DependencyList deps_;
   std::optional<std::string> compiler_target_;
   std::vector<std::string> compiler_args_;
+  std::optional<OutputFile> proc_macro_dynamic_library_;
 };
 
 using CrateList = std::vector<Crate>;
diff --git a/src/gn/rust_project_writer_unittest.cc b/src/gn/rust_project_writer_unittest.cc
index 675f9a6..77a9d34 100644
--- a/src/gn/rust_project_writer_unittest.cc
+++ b/src/gn/rust_project_writer_unittest.cc
@@ -521,4 +521,57 @@
       "}\n";
 
   ExpectEqOrShowDiff(expected_json, out);
-}
\ No newline at end of file
+}
+
+TEST_F(RustProjectJSONWriter, OneRustProcMacroTarget) {
+  Err err;
+  TestWithScope setup;
+  setup.build_settings()->SetRootPath(UTF8ToFilePath("path"));
+
+  Target target(setup.settings(), Label(SourceDir("//foo/"), "bar"));
+  target.set_output_type(Target::RUST_PROC_MACRO);
+  target.visibility().SetPublic();
+  SourceFile lib("//foo/lib.rs");
+  target.sources().push_back(lib);
+  target.source_types_used().Set(SourceFile::SOURCE_RS);
+  target.rust_values().set_crate_root(lib);
+  target.rust_values().crate_name() = "foo";
+  target.config_values().rustflags().push_back("--cfg=feature=\"foo_enabled\"");
+  target.SetToolchain(setup.toolchain());
+  ASSERT_TRUE(target.OnResolved(&err));
+
+  std::ostringstream stream;
+  std::vector<const Target*> targets;
+  targets.push_back(&target);
+  RustProjectWriter::RenderJSON(setup.build_settings(), targets, stream);
+  std::string out = stream.str();
+#if defined(OS_WIN)
+  base::ReplaceSubstringsAfterOffset(&out, 0, "\r\n", "\n");
+#endif
+  const char expected_json[] =
+      "{\n"
+      "  \"roots\": [\n"
+      "    \"path/foo/\"\n"
+      "  ],\n"
+      "  \"crates\": [\n"
+      "    {\n"
+      "      \"crate_id\": 0,\n"
+      "      \"root_module\": \"path/foo/lib.rs\",\n"
+      "      \"label\": \"//foo:bar\",\n"
+      "      \"compiler_args\": [\"--cfg=feature=\\\"foo_enabled\\\"\"],\n"
+      "      \"deps\": [\n"
+      "      ],\n"
+      "      \"edition\": \"2015\",\n"
+      "      \"is_proc_macro\": true,\n"
+      "      \"proc_macro_dylib_path\": \"path/out/Debug/obj/foo/libbar.so\",\n"
+      "      \"cfg\": [\n"
+      "        \"test\",\n"
+      "        \"debug_assertions\",\n"
+      "        \"feature=\\\"foo_enabled\\\"\"\n"
+      "      ]\n"
+      "    }\n"
+      "  ]\n"
+      "}\n";
+
+  ExpectEqOrShowDiff(expected_json, out);
+}