Create the skeleton of the C++ -> Rust migration tool.

PiperOrigin-RevId: 448016954
diff --git a/common/file_io.cc b/common/file_io.cc
index 2975dd3..613b642 100644
--- a/common/file_io.cc
+++ b/common/file_io.cc
@@ -4,10 +4,21 @@
 
 #include "common/file_io.h"
 
+#include "llvm/Support/MemoryBuffer.h"
 #include "llvm/Support/raw_ostream.h"
 
 namespace crubit {
 
+absl::StatusOr<std::string> GetFileContents(absl::string_view path) {
+  llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> err_or_buffer =
+      llvm::MemoryBuffer::getFileOrSTDIN(path.data(), /* IsText= */ true);
+  if (std::error_code err = err_or_buffer.getError()) {
+    return absl::Status(absl::StatusCode::kInternal, err.message());
+  }
+
+  return std::string((*err_or_buffer)->getBuffer());
+}
+
 absl::Status SetFileContents(absl::string_view path,
                              absl::string_view contents) {
   std::error_code error_code;
diff --git a/common/file_io.h b/common/file_io.h
index 7ab9ff8..b2924d6 100644
--- a/common/file_io.h
+++ b/common/file_io.h
@@ -11,6 +11,8 @@
 
 namespace crubit {
 
+absl::StatusOr<std::string> GetFileContents(absl::string_view path);
+
 absl::Status SetFileContents(absl::string_view path,
                              absl::string_view contents);
 
diff --git a/migrator/rs_from_cc/BUILD b/migrator/rs_from_cc/BUILD
new file mode 100644
index 0000000..834ae10
--- /dev/null
+++ b/migrator/rs_from_cc/BUILD
@@ -0,0 +1,94 @@
+"""Generates equivalent Rust code from C++ code."""
+
+licenses(["notice"])
+
+cc_binary(
+    name = "rs_from_cc",
+    srcs = ["rs_from_cc.cc"],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":rs_from_cc_lib",
+        "@absl//flags:flag",
+        "@absl//flags:parse",
+        "@absl//status",
+        "@absl//status:statusor",
+        "@absl//strings",
+        "//common:check",
+        "//common:file_io",
+        "@llvm///llvm:Support",
+        "@rust//support:rust_okay_here",
+    ],
+)
+
+cc_library(
+    name = "frontend_action",
+    srcs = ["frontend_action.cc"],
+    hdrs = ["frontend_action.h"],
+    deps = [
+        ":ast_consumer",
+        ":converter",
+        "//lifetime_annotations",
+        "@llvm///clang:ast",
+        "@llvm///clang:frontend",
+    ],
+)
+
+cc_library(
+    name = "ast_consumer",
+    srcs = ["ast_consumer.cc"],
+    hdrs = ["ast_consumer.h"],
+    deps = [
+        ":converter",
+        "//common:check",
+        "@llvm///clang:ast",
+        "@llvm///clang:frontend",
+    ],
+)
+
+cc_library(
+    name = "converter",
+    srcs = ["converter.cc"],
+    hdrs = ["converter.h"],
+    deps = [
+        "@absl//container:flat_hash_map",
+        "@absl//container:flat_hash_set",
+        "@absl//status:statusor",
+        "@absl//strings",
+        "@absl//types:span",
+        "//lifetime_annotations",
+        "@llvm///clang:ast",
+        "@llvm///clang:basic",
+        "@llvm///clang:sema",
+        "//third_party/re2",
+    ],
+)
+
+cc_test(
+    name = "rs_from_cc_test",
+    srcs = ["rs_from_cc_lib_test.cc"],
+    deps = [
+        ":rs_from_cc_lib",
+        "//testing/base/public:gunit_main",
+        "@absl//status",
+        "@absl//strings",
+        "@llvm///clang:ast",
+    ],
+)
+
+cc_library(
+    name = "rs_from_cc_lib",
+    srcs = ["rs_from_cc_lib.cc"],
+    hdrs = ["rs_from_cc_lib.h"],
+    deps = [
+        ":converter",
+        ":frontend_action",
+        "@absl//container:flat_hash_map",
+        "@absl//status",
+        "@absl//status:statusor",
+        "@absl//strings",
+        "@absl//types:span",
+        "@llvm///clang:basic",
+        "@llvm///clang:frontend",
+        "@llvm///clang:tooling",
+    ],
+)
diff --git a/migrator/rs_from_cc/README.md b/migrator/rs_from_cc/README.md
new file mode 100644
index 0000000..309cf96
--- /dev/null
+++ b/migrator/rs_from_cc/README.md
@@ -0,0 +1,8 @@
+# rs_from_cc
+
+Disclaimer: This project is experimental, under heavy development, and should
+not be used yet.
+
+`:rs_from_cc` converts C++ code into equivalent Rust code.
+
+The relevant design docs are [here](./docs/).
diff --git a/migrator/rs_from_cc/ast_consumer.cc b/migrator/rs_from_cc/ast_consumer.cc
new file mode 100644
index 0000000..c41bbad
--- /dev/null
+++ b/migrator/rs_from_cc/ast_consumer.cc
@@ -0,0 +1,27 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "migrator/rs_from_cc/ast_consumer.h"
+
+#include "common/check.h"
+#include "migrator/rs_from_cc/converter.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/Frontend/CompilerInstance.h"
+
+namespace crubit_rs_from_cc {
+
+void AstConsumer::HandleTranslationUnit(clang::ASTContext& ast_context) {
+  if (ast_context.getDiagnostics().hasErrorOccurred()) {
+    // We do not need to process partially incorrect headers, we assume all
+    // input is valid C++. If there is an error Clang already printed it to
+    // stderr; the user will be informed about the cause of the failure.
+    // There is nothing more for us to do here.
+    return;
+  }
+  CRUBIT_CHECK(instance_.hasSema());
+  Converter converter(invocation_, ast_context);
+  converter.Convert(ast_context.getTranslationUnitDecl());
+}
+
+}  // namespace crubit_rs_from_cc
diff --git a/migrator/rs_from_cc/ast_consumer.h b/migrator/rs_from_cc/ast_consumer.h
new file mode 100644
index 0000000..7d934ab
--- /dev/null
+++ b/migrator/rs_from_cc/ast_consumer.h
@@ -0,0 +1,32 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#ifndef CRUBIT_MIGRATOR_RS_FROM_CC_AST_CONSUMER_H_
+#define CRUBIT_MIGRATOR_RS_FROM_CC_AST_CONSUMER_H_
+
+#include "migrator/rs_from_cc/converter.h"
+#include "clang/AST/ASTConsumer.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/Frontend/CompilerInstance.h"
+
+namespace crubit_rs_from_cc {
+
+// Consumes the Clang AST created from the invocation's entry header and
+// generates the intermediate representation (`IR`) in the invocation object.
+class AstConsumer : public clang::ASTConsumer {
+ public:
+  explicit AstConsumer(clang::CompilerInstance& instance,
+                       Converter::Invocation& invocation)
+      : instance_(instance), invocation_(invocation) {}
+
+  void HandleTranslationUnit(clang::ASTContext& context) override;
+
+ private:
+  clang::CompilerInstance& instance_;
+  Converter::Invocation& invocation_;
+};  // class AstConsumer
+
+}  // namespace crubit_rs_from_cc
+
+#endif  // CRUBIT_MIGRATOR_RS_FROM_CC_AST_CONSUMER_H_
diff --git a/migrator/rs_from_cc/converter.cc b/migrator/rs_from_cc/converter.cc
new file mode 100644
index 0000000..7cec407
--- /dev/null
+++ b/migrator/rs_from_cc/converter.cc
@@ -0,0 +1,45 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "migrator/rs_from_cc/converter.h"
+
+#include <string>
+
+#include "absl/strings/str_split.h"
+#include "clang/AST/CXXInheritance.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/RecordLayout.h"
+#include "clang/Basic/FileManager.h"
+#include "third_party/re2/re2.h"
+
+namespace crubit_rs_from_cc {
+
+void Converter::Convert(
+    const clang::TranslationUnitDecl* translation_unit_decl) {
+  ConvertUnsupported(translation_unit_decl);
+}
+
+void Converter::ConvertUnsupported(const clang::Decl* decl) {
+  std::string ast;
+  llvm::raw_string_ostream os(ast);
+  decl->dump(os);
+  os.flush();
+  result_ += "\n";
+  result_ += "// Unsupported decl:\n//\n";
+  // Remove addresses since they're not useful and add non-determinism that
+  // would break golden testing.
+  // Also remove spaces at the end of each line, those are a pain in golden
+  // tests since IDEs often strip spaces at end of line.
+  RE2::GlobalReplace(&ast, "(?m) 0x[a-z0-9]+| +$", "");
+  for (auto line : absl::StrSplit(ast, '\n')) {
+    if (line.empty()) {
+      continue;
+    }
+    result_ += "// ";
+    result_ += line;
+    result_ += '\n';
+  }
+}
+
+}  // namespace crubit_rs_from_cc
diff --git a/migrator/rs_from_cc/converter.h b/migrator/rs_from_cc/converter.h
new file mode 100644
index 0000000..25b447c
--- /dev/null
+++ b/migrator/rs_from_cc/converter.h
@@ -0,0 +1,61 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#ifndef CRUBIT_MIGRATOR_RS_FROM_CC_CONVERTER_H_
+#define CRUBIT_MIGRATOR_RS_FROM_CC_CONVERTER_H_
+
+#include <memory>
+#include <optional>
+#include <set>
+#include <string>
+#include <utility>
+#include <variant>
+#include <vector>
+
+#include "absl/container/flat_hash_map.h"
+#include "absl/container/flat_hash_set.h"
+#include "absl/status/statusor.h"
+#include "absl/types/span.h"
+#include "lifetime_annotations/lifetime_annotations.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/Mangle.h"
+#include "clang/AST/RawCommentList.h"
+#include "clang/AST/Type.h"
+#include "clang/Basic/SourceLocation.h"
+#include "clang/Basic/Specifiers.h"
+#include "clang/Sema/Sema.h"
+
+namespace crubit_rs_from_cc {
+
+// Visits the C++ AST and generates the corresponding Rust code in the
+// Invocation object.
+class Converter {
+ public:
+  // Top-level parameters as well as return value of a migrator invocation.
+  class Invocation {
+   public:
+    std::string rs_code_;
+  };
+
+  explicit Converter(Invocation& invocation, clang::ASTContext& ctx)
+      : result_(invocation.rs_code_), ctx_(ctx) {}
+
+  // Import all visible declarations from a translation unit.
+  void Convert(const clang::TranslationUnitDecl* decl);
+
+  // "converts" a not-yet-supported declaration to Rust by dumping the C++ AST
+  // into a comment.
+  void ConvertUnsupported(const clang::Decl* decl);
+
+ private:
+  // The main output of the conversion process (Rust code).
+  std::string& result_;
+
+  clang::ASTContext& ctx_;
+};  // class Converter
+
+}  // namespace crubit_rs_from_cc
+
+#endif  // CRUBIT_MIGRATOR_RS_FROM_CC_CONVERTER_H_
diff --git a/migrator/rs_from_cc/frontend_action.cc b/migrator/rs_from_cc/frontend_action.cc
new file mode 100644
index 0000000..7fbe5f8
--- /dev/null
+++ b/migrator/rs_from_cc/frontend_action.cc
@@ -0,0 +1,20 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "migrator/rs_from_cc/frontend_action.h"
+
+#include <memory>
+
+#include "migrator/rs_from_cc/ast_consumer.h"
+#include "clang/AST/ASTConsumer.h"
+#include "clang/Frontend/CompilerInstance.h"
+
+namespace crubit_rs_from_cc {
+
+std::unique_ptr<clang::ASTConsumer> FrontendAction::CreateASTConsumer(
+    clang::CompilerInstance& instance, llvm::StringRef) {
+  return std::make_unique<AstConsumer>(instance, invocation_);
+}
+
+}  // namespace crubit_rs_from_cc
diff --git a/migrator/rs_from_cc/frontend_action.h b/migrator/rs_from_cc/frontend_action.h
new file mode 100644
index 0000000..42c6779
--- /dev/null
+++ b/migrator/rs_from_cc/frontend_action.h
@@ -0,0 +1,34 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#ifndef CRUBIT_MIGRATOR_RS_FROM_CC_FRONTEND_ACTION_H_
+#define CRUBIT_MIGRATOR_RS_FROM_CC_FRONTEND_ACTION_H_
+
+#include <memory>
+
+#include "lifetime_annotations/lifetime_annotations.h"
+#include "migrator/rs_from_cc/converter.h"
+#include "clang/AST/ASTConsumer.h"
+#include "clang/Frontend/CompilerInstance.h"
+#include "clang/Frontend/FrontendAction.h"
+
+namespace crubit_rs_from_cc {
+
+// Creates an `ASTConsumer` that generates the Rust code in the invocation
+// object.
+class FrontendAction : public clang::ASTFrontendAction {
+ public:
+  explicit FrontendAction(Converter::Invocation& invocation)
+      : invocation_(invocation) {}
+
+  std::unique_ptr<clang::ASTConsumer> CreateASTConsumer(
+      clang::CompilerInstance& instance, llvm::StringRef) override;
+
+ private:
+  Converter::Invocation& invocation_;
+};
+
+}  // namespace crubit_rs_from_cc
+
+#endif  // CRUBIT_MIGRATOR_RS_FROM_CC_FRONTEND_ACTION_H_
diff --git a/migrator/rs_from_cc/rs_from_cc.cc b/migrator/rs_from_cc/rs_from_cc.cc
new file mode 100644
index 0000000..40e7cfd
--- /dev/null
+++ b/migrator/rs_from_cc/rs_from_cc.cc
@@ -0,0 +1,56 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+// Parses C++ code and generates an equivalent Rust source file.
+
+#include <utility>
+#include <vector>
+
+#include "absl/flags/flag.h"
+#include "absl/flags/parse.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/string_view.h"
+#include "common/check.h"
+#include "common/file_io.h"
+#include "migrator/rs_from_cc/rs_from_cc_lib.h"
+#include "llvm/Support/FileSystem.h"
+
+ABSL_FLAG(std::string, cc_in, "",
+          "input path for the C++ source file (it may or may not be a header)");
+ABSL_FLAG(std::string, rs_out, "",
+          "output path for the Rust source file; will be overwritten if it "
+          "already exists");
+
+int main(int argc, char* argv[]) {
+  auto args = absl::ParseCommandLine(argc, argv);
+
+  auto cc_in = absl::GetFlag(FLAGS_cc_in);
+  if (cc_in.empty()) {
+    std::cerr << "please specify --cc_in" << std::endl;
+    return 1;
+  }
+  auto rs_out = absl::GetFlag(FLAGS_rs_out);
+  if (rs_out.empty()) {
+    std::cerr << "please specify --rs_out" << std::endl;
+    return 1;
+  }
+
+  auto status_or_cc_file_content = crubit::GetFileContents(cc_in);
+  CRUBIT_CHECK(status_or_cc_file_content.ok());
+  std::string cc_file_content = std::move(*status_or_cc_file_content);
+
+  // Skip $0.
+  ++argv;
+
+  absl::StatusOr<std::string> rs_code = crubit_rs_from_cc::RsFromCc(
+      cc_file_content, cc_in,
+      std::vector<absl::string_view>(argv, argv + argc));
+  if (!rs_code.ok()) {
+    CRUBIT_CHECK(rs_code.ok());
+  }
+
+  CRUBIT_CHECK(crubit::SetFileContents(rs_out, *rs_code).ok());
+  return 0;
+}
diff --git a/migrator/rs_from_cc/rs_from_cc_lib.cc b/migrator/rs_from_cc/rs_from_cc_lib.cc
new file mode 100644
index 0000000..d25a258
--- /dev/null
+++ b/migrator/rs_from_cc/rs_from_cc_lib.cc
@@ -0,0 +1,48 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "migrator/rs_from_cc/rs_from_cc_lib.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "absl/container/flat_hash_map.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/string_view.h"
+#include "absl/strings/substitute.h"
+#include "absl/types/span.h"
+#include "migrator/rs_from_cc/converter.h"
+#include "migrator/rs_from_cc/frontend_action.h"
+#include "clang/Basic/FileManager.h"
+#include "clang/Basic/FileSystemOptions.h"
+#include "clang/Frontend/FrontendAction.h"
+#include "clang/Tooling/Tooling.h"
+
+namespace crubit_rs_from_cc {
+
+absl::StatusOr<std::string> RsFromCc(const absl::string_view cc_file_content,
+                                     const absl::string_view cc_file_name,
+                                     absl::Span<const absl::string_view> args) {
+  std::vector<std::string> args_as_strings{
+      // Parse non-doc comments that are used as documention
+      "-fparse-all-comments"};
+  args_as_strings.insert(args_as_strings.end(), args.begin(), args.end());
+
+  Converter::Invocation invocation;
+  if (clang::tooling::runToolOnCodeWithArgs(
+          std::make_unique<FrontendAction>(invocation), cc_file_content,
+          args_as_strings, cc_file_name, "rs_from_cc",
+          std::make_shared<clang::PCHContainerOperations>(),
+          clang::tooling::FileContentMappings())) {
+    return invocation.rs_code_;
+  } else {
+    return absl::Status(absl::StatusCode::kInvalidArgument,
+                        "Could not compile source file contents");
+  }
+}
+
+}  // namespace crubit_rs_from_cc
diff --git a/migrator/rs_from_cc/rs_from_cc_lib.h b/migrator/rs_from_cc/rs_from_cc_lib.h
new file mode 100644
index 0000000..8eed34f
--- /dev/null
+++ b/migrator/rs_from_cc/rs_from_cc_lib.h
@@ -0,0 +1,32 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#ifndef CRUBIT_MIGRATOR_RS_FROM_CC_RS_FROM_CC_LIB_H_
+#define CRUBIT_MIGRATOR_RS_FROM_CC_RS_FROM_CC_LIB_H_
+
+#include <string>
+
+#include "absl/container/flat_hash_map.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/string_view.h"
+#include "absl/types/span.h"
+
+namespace crubit_rs_from_cc {
+
+// Converts C++ source code into Rust.
+//
+// Parameters:
+// * `cc_file_content`: a string with the C++ source code to convert.
+// * `cc_file_name`: name of the C++ file we're converting. Can be omitted for
+//   tests.
+// * `args`: additional command line arguments for Clang (if any)
+//
+absl::StatusOr<std::string> RsFromCc(
+    absl::string_view cc_file_content,
+    absl::string_view cc_file_name = "testing/file_name.cc",
+    absl::Span<const absl::string_view> args = {});
+
+}  // namespace crubit_rs_from_cc
+
+#endif  // CRUBIT_MIGRATOR_RS_FROM_CC_RS_FROM_CC_LIB_H_
diff --git a/migrator/rs_from_cc/rs_from_cc_lib_test.cc b/migrator/rs_from_cc/rs_from_cc_lib_test.cc
new file mode 100644
index 0000000..e20826e
--- /dev/null
+++ b/migrator/rs_from_cc/rs_from_cc_lib_test.cc
@@ -0,0 +1,102 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "migrator/rs_from_cc/rs_from_cc_lib.h"
+
+#include <variant>
+
+#include "testing/base/public/gmock.h"
+#include "testing/base/public/gunit.h"
+#include "absl/status/status.h"
+#include "absl/strings/string_view.h"
+#include "clang/AST/ASTContext.h"
+
+namespace crubit_rs_from_cc {
+namespace {
+
+using ::testing::Eq;
+using ::testing::status::StatusIs;
+
+TEST(RsFromCcTest, Noop) {
+  // Nothing interesting there, but also not empty, so that the header gets
+  // generated.
+  ASSERT_OK_AND_ASSIGN(std::string rs_code, RsFromCc(" "));
+
+  EXPECT_THAT(rs_code, Eq(R"end_of_string(
+// Unsupported decl:
+//
+// TranslationUnitDecl <<invalid sloc>> <invalid sloc>
+// |-TypedefDecl <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
+// | `-BuiltinType '__int128'
+// |-TypedefDecl <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128'
+// | `-BuiltinType 'unsigned __int128'
+// |-TypedefDecl <<invalid sloc>> <invalid sloc> implicit __NSConstantString '__NSConstantString_tag'
+// | `-RecordType '__NSConstantString_tag'
+// |   `-CXXRecord '__NSConstantString_tag'
+// |-TypedefDecl <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *'
+// | `-PointerType 'char *'
+// |   `-BuiltinType 'char'
+// `-TypedefDecl <<invalid sloc>> <invalid sloc> implicit __builtin_va_list '__va_list_tag[1]'
+//   `-ConstantArrayType '__va_list_tag[1]' 1
+//     `-RecordType '__va_list_tag'
+//       `-CXXRecord '__va_list_tag'
+)end_of_string"));
+}
+
+TEST(RsFromCcTest, Comment) {
+  ASSERT_OK_AND_ASSIGN(std::string rs_code, RsFromCc("// This is a comment"));
+
+  EXPECT_THAT(rs_code, Eq(R"end_of_string(
+// Unsupported decl:
+//
+// TranslationUnitDecl <<invalid sloc>> <invalid sloc>
+// |-TypedefDecl <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
+// | `-BuiltinType '__int128'
+// |-TypedefDecl <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128'
+// | `-BuiltinType 'unsigned __int128'
+// |-TypedefDecl <<invalid sloc>> <invalid sloc> implicit __NSConstantString '__NSConstantString_tag'
+// | `-RecordType '__NSConstantString_tag'
+// |   `-CXXRecord '__NSConstantString_tag'
+// |-TypedefDecl <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *'
+// | `-PointerType 'char *'
+// |   `-BuiltinType 'char'
+// `-TypedefDecl <<invalid sloc>> <invalid sloc> implicit __builtin_va_list '__va_list_tag[1]'
+//   `-ConstantArrayType '__va_list_tag[1]' 1
+//     `-RecordType '__va_list_tag'
+//       `-CXXRecord '__va_list_tag'
+)end_of_string"));
+}
+
+TEST(RsFromCcTest, ErrorOnInvalidInput) {
+  ASSERT_THAT(RsFromCc("int foo(); But this is not C++"),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(RsFromCcTest, FunctionDeclaration) {
+  ASSERT_OK_AND_ASSIGN(std::string rs_code, RsFromCc("void f();"));
+
+  EXPECT_THAT(rs_code, Eq(R"end_of_string(
+// Unsupported decl:
+//
+// TranslationUnitDecl <<invalid sloc>> <invalid sloc>
+// |-TypedefDecl <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
+// | `-BuiltinType '__int128'
+// |-TypedefDecl <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128'
+// | `-BuiltinType 'unsigned __int128'
+// |-TypedefDecl <<invalid sloc>> <invalid sloc> implicit __NSConstantString '__NSConstantString_tag'
+// | `-RecordType '__NSConstantString_tag'
+// |   `-CXXRecord '__NSConstantString_tag'
+// |-TypedefDecl <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *'
+// | `-PointerType 'char *'
+// |   `-BuiltinType 'char'
+// |-TypedefDecl <<invalid sloc>> <invalid sloc> implicit __builtin_va_list '__va_list_tag[1]'
+// | `-ConstantArrayType '__va_list_tag[1]' 1
+// |   `-RecordType '__va_list_tag'
+// |     `-CXXRecord '__va_list_tag'
+// `-FunctionDecl <testing/file_name.cc:1:1, col:8> col:6 f 'void ()'
+)end_of_string"));
+}
+
+}  // namespace
+}  // namespace crubit_rs_from_cc