Refactor the clang invocations to make them more uniform and robust:

* make sure that we correctly retain the exit code from clang and fail tests on
  errors
* unify the clang invocations into one, so that we can share the command line
  configuration
* fix the command line to correctly use the clang binary name as the first
  entry and replace the faulty `--syntax-only` with `-fsyntax-only`

PiperOrigin-RevId: 402220726
diff --git a/rs_bindings_from_cc/ast_visitor_test.cc b/rs_bindings_from_cc/ast_visitor_test.cc
index 06a46d9..7adc000 100644
--- a/rs_bindings_from_cc/ast_visitor_test.cc
+++ b/rs_bindings_from_cc/ast_visitor_test.cc
@@ -26,6 +26,7 @@
 using ::testing::Property;
 using ::testing::SizeIs;
 using ::testing::VariantWith;
+using ::testing::status::StatusIs;
 
 // Matches an IR node that has the given identifier.
 MATCHER_P(IdentifierIs, identifier, "") {
@@ -207,27 +208,28 @@
 }
 
 TEST(AstVisitorTest, Noop) {
-  IR ir = IrFromCc({"// nothing interesting there."}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({"// nothing interesting there."}));
+
   EXPECT_THAT(ir.items, IsEmpty());
   EXPECT_THAT(ir.used_headers,
               ElementsAre(Property(&HeaderName::IncludePath,
                                    "test/testing_header_0.h")));
 }
 
-TEST(AstVisitorTest, IREmptyOnInvalidInput) {
-  IR ir = IrFromCc({"int foo(); But this is not C++"}, {});
-  EXPECT_THAT(ir.items, IsEmpty());
+TEST(AstVisitorTest, ErrorOnInvalidInput) {
+  ASSERT_THAT(IrFromCc({"int foo(); But this is not C++"}),
+              StatusIs(absl::StatusCode::kInvalidArgument));
 }
 
 TEST(AstVisitorTest, FuncWithVoidReturnType) {
-  IR ir = IrFromCc({"void Foo();"}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({"void Foo();"}));
   EXPECT_THAT(ir.items, ElementsAre(VariantWith<Func>(
                             AllOf(IdentifierIs("Foo"), MangledNameIs("_Z3Foov"),
                                   ReturnType(IsVoid()), ParamsAre()))));
 }
 
 TEST(AstVisitorTest, TwoFuncs) {
-  IR ir = IrFromCc({"void Foo(); void Bar();"}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({"void Foo(); void Bar();"}));
   EXPECT_THAT(
       ir.items,
       ElementsAre(
@@ -238,32 +240,32 @@
 }
 
 TEST(AstVisitorTest, TwoFuncsFromTwoHeaders) {
-  IR ir = IrFromCc({"void Foo();", "void Bar();"}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({"void Foo();", "void Bar();"}));
   EXPECT_THAT(ir.items, ElementsAre(VariantWith<Func>(IdentifierIs("Foo")),
                                     VariantWith<Func>(IdentifierIs("Bar"))));
 }
 
 TEST(AstVisitorTest, NonInlineFunc) {
-  IR ir = IrFromCc({"void Foo() {}"}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({"void Foo() {}"}));
   EXPECT_THAT(ir.items, ElementsAre(VariantWith<Func>(
                             AllOf(IdentifierIs("Foo"), Not(IsInline())))));
 }
 
 TEST(AstVisitorTest, InlineFunc) {
-  IR ir = IrFromCc({"inline void Foo() {}"}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({"inline void Foo() {}"}));
   EXPECT_THAT(
       ir.items,
       ElementsAre(VariantWith<Func>(AllOf(IdentifierIs("Foo"), IsInline()))));
 }
 
 TEST(AstVisitorTest, FuncJustOnce) {
-  IR ir = IrFromCc({"void Foo(); void Foo();"}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({"void Foo(); void Foo();"}));
   EXPECT_THAT(ir.items,
               ElementsAre(VariantWith<Func>(AllOf(IdentifierIs("Foo")))));
 }
 
 TEST(AstVisitorTest, TestImportPointerFunc) {
-  IR ir = IrFromCc({"int* Foo(int* a);"}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({"int* Foo(int* a);"}));
 
   EXPECT_THAT(ir.items,
               ElementsAre(VariantWith<Func>(AllOf(
@@ -271,8 +273,9 @@
 }
 
 TEST(AstVisitorTest, Struct) {
-  IR ir = IrFromCc(
-      {"struct SomeStruct { int first_field; int second_field; };"}, {});
+  ASSERT_OK_AND_ASSIGN(
+      IR ir,
+      IrFromCc({"struct SomeStruct { int first_field; int second_field; };"}));
 
   EXPECT_THAT(ir.items,
               ElementsAre(VariantWith<Record>(AllOf(
@@ -287,7 +290,7 @@
   absl::string_view file =
       "struct Implicit {};\n"
       "struct Defaulted { Defaulted(const Defaulted&) = default; };\n";
-  IR ir = IrFromCc({file}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file}));
 
   EXPECT_THAT(ir.items, SizeIs(2));
   EXPECT_THAT(ir.items, Each(VariantWith<Record>(CopyConstructor(DefinitionIs(
@@ -299,7 +302,7 @@
   // TODO(b/202113881): "struct MemberImplicit { Defined x; };\n"
   // TODO(b/202113881): "struct MemberDefaulted { MemberDefaulted(const
   // MemberDefaulted&) = default; Defined x; };\n"
-  IR ir = IrFromCc({file}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file}));
   EXPECT_THAT(ir.items, SizeIs(1));
   EXPECT_THAT(ir.items, Each(VariantWith<Record>(CopyConstructor(DefinitionIs(
                             SpecialMemberFunc::Definition::kNontrivial)))));
@@ -310,7 +313,7 @@
       "struct Deleted { Deleted(const Deleted&) = delete;};\n"
       // TODO(b/202113881): "struct DeletedByMember { Deleted x; };\n"
       "struct DeletedByCtorDef { DeletedByCtorDef(DeletedByCtorDef&&) {} };\n";
-  IR ir = IrFromCc({file}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file}));
 
   EXPECT_THAT(ir.items, SizeIs(2));
   EXPECT_THAT(ir.items, Each(VariantWith<Record>(CopyConstructor(DefinitionIs(
@@ -322,7 +325,7 @@
       "class Implicit {};\n"
       "struct Defaulted { Defaulted(const Defaulted&) = default; };\n"
       "class Section { public: Section(const Section&) = default; };\n";
-  IR ir = IrFromCc({file}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file}));
 
   EXPECT_THAT(ir.items, SizeIs(3));
   EXPECT_THAT(ir.items,
@@ -333,7 +336,7 @@
   absl::string_view file =
       "class Defaulted { Defaulted(const Defaulted&) = default; };\n"
       "struct Section { private: Section(const Section&) = default; };\n";
-  IR ir = IrFromCc({file}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file}));
 
   EXPECT_THAT(ir.items, SizeIs(2));
   EXPECT_THAT(ir.items,
@@ -344,7 +347,7 @@
   absl::string_view file =
       "struct Implicit {};\n"
       "struct Defaulted { Defaulted(Defaulted&&) = default; };\n";
-  IR ir = IrFromCc({file}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file}));
 
   EXPECT_THAT(ir.items, SizeIs(2));
   EXPECT_THAT(ir.items, Each(VariantWith<Record>(MoveConstructor(DefinitionIs(
@@ -356,7 +359,7 @@
   // TODO(b/202113881): "struct MemberImplicit { Defined x; };\n"
   // TODO(b/202113881): "struct MemberDefaulted { MemberDefaulted(
   // MemberDefaulted&&) = default; Defined x; };\n"
-  IR ir = IrFromCc({file}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file}));
   EXPECT_THAT(ir.items, SizeIs(1));
   EXPECT_THAT(ir.items, Each(VariantWith<Record>(MoveConstructor(DefinitionIs(
                             SpecialMemberFunc::Definition::kNontrivial)))));
@@ -368,7 +371,7 @@
       // TODO(b/202113881): "struct DeletedByMember { Deleted x; };\n"
       "struct SuppressedByCtorDef {"
       " SuppressedByCtorDef(const SuppressedByCtorDef&) {}};\n";
-  IR ir = IrFromCc({file}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file}));
 
   EXPECT_THAT(ir.items, SizeIs(2));
   EXPECT_THAT(ir.items, Each(VariantWith<Record>(MoveConstructor(DefinitionIs(
@@ -380,7 +383,7 @@
       "class Implicit {};\n"
       "struct Defaulted { Defaulted(Defaulted&&) = default; };\n"
       "class Section { public: Section(Section&&) = default; };\n";
-  IR ir = IrFromCc({file}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file}));
 
   EXPECT_THAT(ir.items, SizeIs(3));
   EXPECT_THAT(ir.items,
@@ -391,7 +394,7 @@
   absl::string_view file =
       "class Defaulted { Defaulted(Defaulted&&) = default; };\n"
       "struct Section { private: Section(Section&&) = default; };\n";
-  IR ir = IrFromCc({file}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file}));
 
   EXPECT_THAT(ir.items, SizeIs(2));
   EXPECT_THAT(ir.items,
@@ -402,7 +405,7 @@
   absl::string_view file =
       "struct Implicit {};\n"
       "struct Defaulted { ~Defaulted() = default; };\n";
-  IR ir = IrFromCc({file}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file}));
 
   EXPECT_THAT(ir.items, SizeIs(2));
   EXPECT_THAT(ir.items, Each(VariantWith<Record>(Destructor(DefinitionIs(
@@ -414,7 +417,7 @@
   // TODO(b/202113881): "struct MemberImplicit { Defined x; };\n"
   // TODO(b/202113881): "struct MemberDefaulted { ~MemberDefaulted() = default;
   // Defined x; };\n"
-  IR ir = IrFromCc({file}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file}));
   EXPECT_THAT(ir.items, SizeIs(1));
   EXPECT_THAT(ir.items, Each(VariantWith<Record>(Destructor(DefinitionIs(
                             SpecialMemberFunc::Definition::kNontrivial)))));
@@ -423,7 +426,7 @@
 TEST(AstVisitorTest, DeletedDestructor) {
   absl::string_view file = "struct Deleted { ~Deleted() = delete;};\n";
   // TODO(b/202113881): "struct DeletedByMember { Deleted x; };\n"
-  IR ir = IrFromCc({file}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file}));
 
   EXPECT_THAT(ir.items, SizeIs(1));
   EXPECT_THAT(ir.items, Each(VariantWith<Record>(Destructor(DefinitionIs(
@@ -435,7 +438,7 @@
       "class Implicit {};\n"
       "struct Defaulted { ~Defaulted() = default; };\n"
       "class Section { public: ~Section() = default; };\n";
-  IR ir = IrFromCc({file}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file}));
 
   EXPECT_THAT(ir.items, SizeIs(3));
   EXPECT_THAT(ir.items,
@@ -446,7 +449,7 @@
   absl::string_view file =
       "class Defaulted { ~Defaulted() = default; };\n"
       "struct Section { private: ~Section() = default; };\n";
-  IR ir = IrFromCc({file}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file}));
 
   EXPECT_THAT(ir.items, SizeIs(2));
   EXPECT_THAT(ir.items,
@@ -463,7 +466,7 @@
       Nontrivial(const Nontrivial&) {}
     };
   )cc";
-  IR ir = IrFromCc({file}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file}));
 
   EXPECT_THAT(ir.items, SizeIs(3));
   EXPECT_THAT(ir.items, Each(VariantWith<Record>(IsTrivialAbi())));
@@ -475,14 +478,14 @@
       Nontrivial(const Nontrivial&) {}
     };
   )cc";
-  IR ir = IrFromCc({file}, {});
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file}));
 
   EXPECT_THAT(ir.items, SizeIs(1));
   EXPECT_THAT(ir.items, Each(VariantWith<Record>(Not(IsTrivialAbi()))));
 }
 
 TEST(AstVisitorTest, MemberVariableAccessSpecifiers) {
-  IR ir = IrFromCc({R"(
+  ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({R"(
     struct SomeStruct {
       int default_access_int;
     public:
@@ -496,8 +499,7 @@
     class SomeClass {
       int default_access_int;
     };
-  )"},
-                   {});
+  )"}));
 
   EXPECT_THAT(
       ir.items,
diff --git a/rs_bindings_from_cc/ir_from_cc.cc b/rs_bindings_from_cc/ir_from_cc.cc
index b32e8e4..8ff68dd 100644
--- a/rs_bindings_from_cc/ir_from_cc.cc
+++ b/rs_bindings_from_cc/ir_from_cc.cc
@@ -12,11 +12,13 @@
 namespace rs_bindings_from_cc {
 
 static constexpr absl::string_view kVirtualInputPath =
-    "ast_visitor_test_virtual_input.cc";
+    "ir_from_cc_virtual_input.cc";
 
-IR IrFromCc(absl::Span<const absl::string_view> header_files_contents,
-            const std::vector<absl::string_view>& args) {
-  std::vector<std::string> headers;
+absl::StatusOr<IR> IrFromCc(
+    absl::Span<const absl::string_view> header_files_contents,
+    absl::Span<const absl::string_view> header_names,
+    absl::Span<const absl::string_view> args) {
+  std::vector<std::string> headers{header_names.begin(), header_names.end()};
   absl::flat_hash_map<std::string, std::string> file_contents;
   std::string virtual_input_file_content;
 
@@ -25,27 +27,39 @@
     std::string filename(
         absl::Substitute("test/testing_header_$0.h", counter++));
     file_contents.insert({filename, std::string(header_content)});
-    absl::SubstituteAndAppend(&virtual_input_file_content, "#include \"$0\"\n",
-                              filename);
     headers.push_back(std::move(filename));
   }
 
+  for (const std::string& filename : headers) {
+    absl::SubstituteAndAppend(&virtual_input_file_content, "#include \"$0\"\n",
+                              filename);
+  }
   file_contents.insert(
       {std::string(kVirtualInputPath), virtual_input_file_content});
 
-  std::vector<std::string> args_as_strings(args.begin(), args.end());
-  args_as_strings.push_back("--syntax-only");
-  // Needed, so that we can copy over non-doc comments that are used as
-  // documention.
-  args_as_strings.push_back("-fparse-all-comments");
+  std::vector<std::string> args_as_strings{
+      // This includes the path of the binary that we're pretending to be.
+      // TODO(forster): Find out where to point this. Should we use the
+      // production crosstool for this? See
+      // http://google3/devtools/cymbal/common/clang_tool.cc;l=171;rcl=385188113
+      "clang",
+      // We only need the AST.
+      "-fsyntax-only",
+      // Parse non-doc comments that are used as documention.
+      "-fparse-all-comments"};
+  args_as_strings.insert(args_as_strings.end(), args.begin(), args.end());
   args_as_strings.push_back(std::string(kVirtualInputPath));
 
-  IR ir;
-  devtools::cymbal::RunToolWithClangFlagsOnCode(
-      args_as_strings, file_contents,
-      std::make_unique<rs_bindings_from_cc::FrontendAction>(
-          std::vector<absl::string_view>(headers.begin(), headers.end()), ir));
-  return ir;
+  if (IR ir; devtools::cymbal::RunToolWithClangFlagsOnCode(
+          args_as_strings, file_contents,
+          std::make_unique<FrontendAction>(
+              std::vector<absl::string_view>(headers.begin(), headers.end()),
+              ir))) {
+    return ir;
+  } else {
+    return absl::Status(absl::StatusCode::kInvalidArgument,
+                        "Could not compile header contents");
+  }
 }
 
 }  // namespace rs_bindings_from_cc
diff --git a/rs_bindings_from_cc/ir_from_cc.h b/rs_bindings_from_cc/ir_from_cc.h
index 5e6c8c7..19834c6 100644
--- a/rs_bindings_from_cc/ir_from_cc.h
+++ b/rs_bindings_from_cc/ir_from_cc.h
@@ -8,13 +8,24 @@
 #include <vector>
 
 #include "rs_bindings_from_cc/ir.h"
+#include "third_party/absl/status/statusor.h"
 #include "third_party/absl/strings/string_view.h"
 #include "third_party/absl/types/span.h"
 
 namespace rs_bindings_from_cc {
 
-IR IrFromCc(absl::Span<const absl::string_view> header_files_contents,
-            const std::vector<absl::string_view>& args);
+/// Parses C++ source code into IR.
+///
+/// Parameters:
+/// * `header_file_contents`: textual C++ source code to be parsed directly
+/// * `header_names`: names of headers to include from the file system before
+///    the textual source
+/// * `args`: additional command line arguments for Clang
+///
+absl::StatusOr<IR> IrFromCc(
+    absl::Span<const absl::string_view> header_files_contents,
+    absl::Span<const absl::string_view> header_names = {},
+    absl::Span<const absl::string_view> args = {});
 
 }  // namespace rs_bindings_from_cc
 
diff --git a/rs_bindings_from_cc/json_from_cc.cc b/rs_bindings_from_cc/json_from_cc.cc
index 0c19eb9..37e4d1d 100644
--- a/rs_bindings_from_cc/json_from_cc.cc
+++ b/rs_bindings_from_cc/json_from_cc.cc
@@ -4,13 +4,19 @@
 
 #include "rs_bindings_from_cc/ffi_types.h"
 #include "rs_bindings_from_cc/ir_from_cc.h"
+#include "util/task/status.h"
 
 namespace rs_bindings_from_cc {
 
 // This is intended to be called from Rust.
 extern "C" FfiU8SliceBox json_from_cc(FfiU8Slice cc_source) {
-  IR ir = IrFromCc({StringViewFromFfiU8Slice(cc_source)}, {});
-  std::string json = ir.ToJson().dump();
+  absl::StatusOr<IR> ir = IrFromCc({StringViewFromFfiU8Slice(cc_source)});
+  // TODO(forster): For now it is good enough to just exit: We are just using
+  // this from tests, which are ok to just fail. Clang has already printed error
+  // messages. If we start using this for production, then we should bridge the
+  // error code into Rust.
+  CHECK_OK(ir);
+  std::string json = ir->ToJson().dump();
   return AllocFfiU8SliceBox(MakeFfiU8Slice(json));
 }
 
diff --git a/rs_bindings_from_cc/rs_bindings_from_cc.cc b/rs_bindings_from_cc/rs_bindings_from_cc.cc
index e45dd2d..b5ce039 100644
--- a/rs_bindings_from_cc/rs_bindings_from_cc.cc
+++ b/rs_bindings_from_cc/rs_bindings_from_cc.cc
@@ -15,6 +15,7 @@
 #include "devtools/cymbal/common/clang_tool.h"
 #include "rs_bindings_from_cc/frontend_action.h"
 #include "rs_bindings_from_cc/ir.h"
+#include "rs_bindings_from_cc/ir_from_cc.h"
 #include "rs_bindings_from_cc/src_code_gen.h"
 #include "file/base/filesystem.h"
 #include "file/base/helpers.h"
@@ -38,9 +39,6 @@
           "for, in a format suitable for usage in google3-relative quote "
           "include (#include \"\").");
 
-constexpr absl::string_view kVirtualInputPath =
-    "rs_bindings_from_cc_virtual_input.cc";
-
 int main(int argc, char* argv[]) {
   InitGoogle(argv[0], &argc, &argv, true);
 
@@ -54,34 +52,20 @@
 
   auto ir_out = absl::GetFlag(FLAGS_ir_out);  // Optional.
 
-  std::vector<std::string> command_line(argv, argv + argc);
-  // Needed, so that we can copy over non-doc comments that are used as
-  // documention.
-  command_line.push_back("-fparse-all-comments");
-  command_line.push_back(std::string(kVirtualInputPath));
-
-  std::string virtual_input_file_content;
-  for (const std::string& header : public_headers) {
-    absl::SubstituteAndAppend(&virtual_input_file_content, "#include \"$0\"\n",
-                              header);
-  }
-
-  absl::flat_hash_map<std::string, std::string> file_contents{
-      {std::string(kVirtualInputPath), virtual_input_file_content}};
-
-  rs_bindings_from_cc::IR ir;
-  if (devtools::cymbal::RunToolWithClangFlagsOnCode(
-          command_line, file_contents,
-          std::make_unique<rs_bindings_from_cc::FrontendAction>(
+  if (absl::StatusOr<rs_bindings_from_cc::IR> ir =
+          rs_bindings_from_cc::IrFromCc(
+              {},
               std::vector<absl::string_view>(public_headers.begin(),
                                              public_headers.end()),
-              ir))) {
+
+              std::vector<absl::string_view>(argv, argv + argc));
+      ir.ok()) {
     if (!ir_out.empty()) {
-      CHECK_OK(file::SetContents(ir_out, ir.ToJson().dump(/*indent=*/2),
+      CHECK_OK(file::SetContents(ir_out, ir->ToJson().dump(/*indent=*/2),
                                  file::Defaults()));
     }
     rs_bindings_from_cc::Bindings bindings =
-        rs_bindings_from_cc::GenerateBindings(ir);
+        rs_bindings_from_cc::GenerateBindings(*ir);
     CHECK_OK(file::SetContents(rs_out, bindings.rs_api, file::Defaults()));
     CHECK_OK(file::SetContents(cc_out, bindings.rs_api_impl, file::Defaults()));
     return 0;