From 80975f347ed42dfd476abd259192f61fc6108677 Mon Sep 17 00:00:00 2001 From: Googler Date: Thu, 5 Feb 2026 14:36:29 -0800 Subject: [PATCH] Internal change PiperOrigin-RevId: 866117160 --- common/crubit_feature.rs | 9 ++ features/BUILD | 10 ++ rs_bindings_from_cc/BUILD | 2 + rs_bindings_from_cc/decl_importer.h | 22 ++++ rs_bindings_from_cc/importer.cc | 135 +++++++++++++++++++- rs_bindings_from_cc/importer.h | 9 ++ rs_bindings_from_cc/importer_test.cc | 126 ++++++++++++++++++ rs_bindings_from_cc/importers/BUILD | 1 + rs_bindings_from_cc/importers/cxx_record.cc | 2 + rs_bindings_from_cc/importers/enum.cc | 6 +- rs_bindings_from_cc/ir.cc | 2 + rs_bindings_from_cc/ir.h | 2 + rs_bindings_from_cc/ir.rs | 9 ++ rs_bindings_from_cc/ir_from_cc_test.rs | 35 +++++ 14 files changed, 366 insertions(+), 4 deletions(-) diff --git a/common/crubit_feature.rs b/common/crubit_feature.rs index 80db3ead1..059d96959 100644 --- a/common/crubit_feature.rs +++ b/common/crubit_feature.rs @@ -36,6 +36,9 @@ flagset::flags! { /// Use ergonomic lifetime defaults when interpreting lifetime annotations. AssumeLifetimes, + + /// Enable formatting to Rust via C++. + Fmt, } } @@ -55,6 +58,7 @@ impl CrubitFeature { Self::Experimental => "experimental", Self::CustomFfiTypes => "custom_ffi_types", Self::AssumeLifetimes => "assume_lifetimes", + Self::Fmt => "fmt", } } @@ -71,6 +75,7 @@ impl CrubitFeature { Self::Experimental => "//features:experimental", Self::CustomFfiTypes => "//features:custom_ffi_types", Self::AssumeLifetimes => "//features:assume_lifetimes", + Self::Fmt => "//features:fmt", } } } @@ -88,6 +93,7 @@ pub fn named_features(name: &[u8]) -> Option> { b"experimental" => CrubitFeature::Experimental.into(), b"custom_ffi_types" => CrubitFeature::CustomFfiTypes.into(), b"assume_lifetimes" => CrubitFeature::AssumeLifetimes.into(), + b"fmt" => CrubitFeature::Fmt.into(), _ => return None, }; Some(features) @@ -195,6 +201,7 @@ mod tests { | CrubitFeature::NonUnpinCtor | CrubitFeature::Experimental | CrubitFeature::CustomFfiTypes + | CrubitFeature::Fmt ); } @@ -223,6 +230,7 @@ mod tests { | CrubitFeature::NonUnpinCtor | CrubitFeature::Experimental | CrubitFeature::CustomFfiTypes + | CrubitFeature::Fmt ); } @@ -239,6 +247,7 @@ mod tests { | CrubitFeature::NonUnpinCtor | CrubitFeature::Experimental | CrubitFeature::CustomFfiTypes + | CrubitFeature::Fmt ); } } diff --git a/features/BUILD b/features/BUILD index a18263100..68a219af2 100644 --- a/features/BUILD +++ b/features/BUILD @@ -94,6 +94,16 @@ crubit_feature_hint( visibility = ["//visibility:public"], ) +# A feature set enabling formatting to Rust via C++. +# +# See crubit.rs-features#other +crubit_feature_hint( + name = "fmt", + compatible_with = ["//buildenv/target:non_prod"], + crubit_features = SUPPORTED_FEATURES + ["fmt"], + visibility = ["//visibility:public"], +) + # A feature set containing experimental Crubit features, in addition to the officially supported # features. # diff --git a/rs_bindings_from_cc/BUILD b/rs_bindings_from_cc/BUILD index 6f307a009..91102ee79 100644 --- a/rs_bindings_from_cc/BUILD +++ b/rs_bindings_from_cc/BUILD @@ -326,6 +326,7 @@ cc_library( "//rs_bindings_from_cc/importers:var", "@abseil-cpp//absl/algorithm:container", "@abseil-cpp//absl/base:no_destructor", + "@abseil-cpp//absl/base:nullability", "@abseil-cpp//absl/container:flat_hash_map", "@abseil-cpp//absl/container:flat_hash_set", "@abseil-cpp//absl/log", @@ -356,6 +357,7 @@ crubit_cc_test( "//testing/base/public:gunit_main", "@abseil-cpp//absl/functional:overload", "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", "@abseil-cpp//absl/strings", ], ) diff --git a/rs_bindings_from_cc/decl_importer.h b/rs_bindings_from_cc/decl_importer.h index 4d9eebd22..89feaeee4 100644 --- a/rs_bindings_from_cc/decl_importer.h +++ b/rs_bindings_from_cc/decl_importer.h @@ -20,6 +20,7 @@ #include "lifetime_annotations/type_lifetimes.h" #include "rs_bindings_from_cc/bazel_types.h" #include "rs_bindings_from_cc/ir.h" +#include "clang/AST/Decl.h" #include "clang/AST/DeclBase.h" #include "clang/AST/DeclTemplate.h" #include "clang/AST/RawCommentList.h" @@ -172,6 +173,27 @@ class ImportContext { virtual bool AreAssumedLifetimesEnabledForTarget( const BazelLabel& label) const = 0; + // Returns true iff `label` has opted in to formatter detection. + virtual bool IsFmtEnabledForTarget(const BazelLabel& label) const = 0; + + // Returns whether the given `decl`'s type has a detectable formatter. + // + // For a type `T`, finds: + // * `template void AbslStringify(Sink&, const T&)` + // * `template void AbslStringify(Sink&, T)` + // * `std::ostream& operator<<(std::ostream&, const T&)` + // * `std::ostream& operator<<(std::ostream&, T)` + // + // in any of the following: + // * `T`'s namespace + // * `T`'s friends + // * `T`'s bases' friends + // + // and recurses on the bases of `T`. e.g., if `T` inherits from `B`, then + // include both `AbslStringify(Sink&, T)` and `AbslStringify(Sink&, B)` in + // `B`. + virtual bool DetectFormatter(const clang::TypeDecl& decl) const = 0; + // Gets an IR UnqualifiedIdentifier for the named decl. // // If the decl's name is an identifier, this returns that identifier as-is. diff --git a/rs_bindings_from_cc/importer.cc b/rs_bindings_from_cc/importer.cc index f78dc629b..cf38dd20d 100644 --- a/rs_bindings_from_cc/importer.cc +++ b/rs_bindings_from_cc/importer.cc @@ -20,6 +20,7 @@ #include "absl/algorithm/container.h" #include "absl/base/no_destructor.h" +#include "absl/base/nullability.h" #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "absl/log/check.h" @@ -54,6 +55,7 @@ #include "clang/AST/PrettyPrinter.h" #include "clang/AST/RawCommentList.h" #include "clang/AST/Type.h" +#include "clang/AST/TypeBase.h" #include "clang/Basic/AttrKinds.h" #include "clang/Basic/Diagnostic.h" #include "clang/Basic/FileManager.h" @@ -68,6 +70,7 @@ #include "llvm/Support/Casting.h" #include "llvm/Support/FormatVariadic.h" #include "llvm/Support/Regex.h" +#include "llvm/Support/raw_ostream.h" namespace crubit { namespace { @@ -121,6 +124,79 @@ bool IsBuiltinFunction(const clang::Decl* decl) { return function->getBuiltinID() != 0; } +bool IsTypeValueOrRefToConst(clang::QualType candidate, + clang::CanQualType target) { + return candidate->getCanonicalTypeUnqualified() == target || + (candidate->isLValueReferenceType() && + candidate.getNonReferenceType().getCanonicalType() == + target.withConst()); +} + +bool IsLvalueRefToUnqualified(clang::QualType type) { + return type->isLValueReferenceType() && + !type.getNonReferenceType().getCanonicalType().hasQualifiers(); +} + +bool IsStdTemplate(const clang::TemplateDecl& decl, clang::StringRef name) { + const auto* class_template = + clang::dyn_cast(&decl); + return class_template != nullptr && class_template->getName() == name && + class_template->isInStdNamespace(); +} + +bool IsCharTraitsChar(clang::QualType type) { + const clang::TemplateSpecializationType* specialization = + type->getAsNonAliasTemplateSpecializationType(); + return specialization != nullptr && + IsStdTemplate(*specialization->getTemplateName().getAsTemplateDecl(), + "char_traits") && + specialization->template_arguments().size() == 1 && + specialization->template_arguments()[0].getAsType()->isCharType(); +} + +bool IsOstreamRef(clang::QualType type) { + if (!IsLvalueRefToUnqualified(type)) { + return false; + } + const clang::TemplateSpecializationType* specialization = + type.getNonReferenceType()->getAsNonAliasTemplateSpecializationType(); + return specialization != nullptr && + IsStdTemplate(*specialization->getTemplateName().getAsTemplateDecl(), + "basic_ostream") && + !specialization->template_arguments().empty() && + specialization->template_arguments()[0].getAsType()->isCharType() && + (specialization->template_arguments().size() == 1 || + IsCharTraitsChar( + specialization->template_arguments()[1].getAsType())); +} + +bool DetectFormatterFunction(const clang::Decl& decl, + clang::CanQualType formatted_type) { + if (const auto* function_template_decl = + clang::dyn_cast(&decl); + function_template_decl != nullptr) { + return function_template_decl->getName() == "AbslStringify" && + function_template_decl->getAsFunction()->getNumParams() == 2 && + IsTypeValueOrRefToConst( + /*candidate=*/function_template_decl->getAsFunction() + ->getParamDecl(1) + ->getType(), + /*target=*/formatted_type); + } + if (const auto* function_decl = + clang::dyn_cast(&decl); + function_decl != nullptr) { + return function_decl->getOverloadedOperator() == clang::OO_LessLess && + function_decl->getNumParams() == 2 && + IsOstreamRef(function_decl->getParamDecl(0)->getType()) && + IsTypeValueOrRefToConst( + /*candidate=*/function_decl->getParamDecl(1)->getType(), + /*target=*/formatted_type) && + IsOstreamRef(function_decl->getReturnType()); + } + return false; +} + } // namespace namespace { @@ -893,11 +969,64 @@ bool Importer::IsFromProtoTarget(const clang::Decl& decl) const { return filename.has_value() && filename->ends_with(".proto.h"); } -bool Importer::AreAssumedLifetimesEnabledForTarget( - const BazelLabel& label) const { +bool Importer::IsFeatureEnabledForTarget(const BazelLabel& label, + absl::string_view feature) const { if (auto i = invocation_.ir_.crubit_features.find(label); i != invocation_.ir_.crubit_features.end()) { - return i->second.contains("assume_lifetimes"); + return i->second.contains(feature); + } + return false; +} + +bool Importer::AreAssumedLifetimesEnabledForTarget( + const BazelLabel& label) const { + return IsFeatureEnabledForTarget(label, "assume_lifetimes"); +} + +bool Importer::IsFmtEnabledForTarget(const BazelLabel& label) const { + return IsFeatureEnabledForTarget(label, "fmt"); +} + +bool Importer::DetectFormatter(const clang::TypeDecl& decl) const { + clang::CanQualType type = ctx_.getCanonicalTypeDeclType(&decl); + return DetectFormatterForType(/*lookup=*/type, /*target=*/type); +} + +bool Importer::DetectFormatterForType(clang::CanQualType lookup, + clang::CanQualType target) const { + if (const clang::CXXRecordDecl* record = lookup->getAsCXXRecordDecl(); + record != nullptr) { + for (const clang::FriendDecl* absl_nonnull friend_decl : + record->friends()) { + const clang::NamedDecl* inner_friend = friend_decl->getFriendDecl(); + if (inner_friend != nullptr && + DetectFormatterFunction(*inner_friend, target)) { + return true; + } + } + for (const clang::CXXBaseSpecifier& base_specifier : record->bases()) { + clang::CanQualType base_type = + ctx_.getCanonicalType(base_specifier.getType()); + if (DetectFormatterForType(/*lookup=*/base_type, /*target=*/target) || + DetectFormatterForType(/*lookup=*/base_type, /*target=*/base_type)) { + return true; + } + } + } + const clang::Type* lookup_type = lookup.getTypePtrOrNull(); + if (lookup_type == nullptr) { + return false; + } + const clang::TagDecl* lookup_decl = lookup_type->getAsTagDecl(); + if (lookup_decl == nullptr) { + return false; + } + const clang::DeclContext* absl_nonnull context = + lookup_decl->getDeclContext()->getEnclosingNamespaceContext(); + for (const clang::Decl* absl_nonnull decl : context->decls()) { + if (DetectFormatterFunction(*decl, target)) { + return true; + } } return false; } diff --git a/rs_bindings_from_cc/importer.h b/rs_bindings_from_cc/importer.h index aa85b9aad..0f00aca10 100644 --- a/rs_bindings_from_cc/importer.h +++ b/rs_bindings_from_cc/importer.h @@ -16,6 +16,7 @@ #include "absl/log/check.h" #include "absl/log/die_if_null.h" #include "absl/status/statusor.h" +#include "absl/strings/string_view.h" #include "lifetime_annotations/type_lifetimes.h" #include "rs_bindings_from_cc/bazel_types.h" #include "rs_bindings_from_cc/decl_importer.h" @@ -119,6 +120,8 @@ class Importer final : public ImportContext { bool IsCrubitEnabledForTarget(const BazelLabel& label) const override; bool AreAssumedLifetimesEnabledForTarget( const BazelLabel& label) const override; + bool IsFmtEnabledForTarget(const BazelLabel& label) const override; + bool DetectFormatter(const clang::TypeDecl& decl) const override; absl::StatusOr GetTranslatedName( const clang::NamedDecl* named_decl) const override; absl::StatusOr GetTranslatedIdentifier( @@ -189,6 +192,12 @@ class Importer final : public ImportContext { CcType ConvertTemplateSpecializationType( const clang::TemplateSpecializationType* type); + bool IsFeatureEnabledForTarget(const BazelLabel& label, + absl::string_view feature) const; + + bool DetectFormatterForType(clang::CanQualType lookup, + clang::CanQualType target) const; + // The different decl importers. Note that order matters: the first importer // to successfully match a decl "wins", and no other importers are tried. std::vector> decl_importers_; diff --git a/rs_bindings_from_cc/importer_test.cc b/rs_bindings_from_cc/importer_test.cc index d1ed80fbb..114f469ff 100644 --- a/rs_bindings_from_cc/importer_test.cc +++ b/rs_bindings_from_cc/importer_test.cc @@ -11,6 +11,7 @@ #include "gtest/gtest.h" #include "absl/functional/overload.h" #include "absl/status/status.h" +#include "absl/status/statusor.h" #include "absl/strings/match.h" #include "absl/strings/string_view.h" #include "common/status_test_matchers.h" @@ -101,6 +102,10 @@ MATCHER_P(DocCommentIs, doc_comment, "") { return false; } +MATCHER(HasDetectedFormatter, "has detected_formatter() == true") { + return arg.detected_formatter; +} + // Matches a Func that has the given mangled name. MATCHER_P(MangledNameIs, mangled_name, "") { if (arg.mangled_name == mangled_name) return true; @@ -1036,6 +1041,127 @@ TEST(ImporterTest, CrashRepro_AutoInvolvingTemplate) { ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file})); } +absl::StatusOr IrFromCcWithFmt(absl::string_view program) { + return IrFromCc(IrFromCcOptions{ + .extra_source_code_for_testing = program, + .crubit_features = {{BazelLabel{"//test:testing_target"}, {"fmt"}}}}); +} + +TEST(ImporterTest, DetectsFormatterAsAbslStringify) { + ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCcWithFmt(R"cc( + struct ByRef { + template + friend void AbslStringify(Sink&, const ByRef&) {} + }; + struct ByValue { + template + friend void AbslStringify(Sink&, ByValue) {} + }; + struct NoFormatter {}; + )cc")); + EXPECT_THAT( + ir.get_items_if(), + AllOf( + Contains(Pointee(AllOf(RsNameIs("ByRef"), HasDetectedFormatter()))), + Contains(Pointee(AllOf(RsNameIs("ByValue"), HasDetectedFormatter()))), + Contains(Pointee( + AllOf(RsNameIs("NoFormatter"), Not(HasDetectedFormatter())))))); +} + +TEST(ImporterTest, DetectsFormatterAsOstream) { + ASSERT_OK_AND_ASSIGN( // + const IR ir, // + IrFromCcWithFmt(R"cc( + namespace std { + template + struct char_traits {}; + template > + struct basic_ostream {}; + using ostream = basic_ostream; + } // namespace std + + struct ByRef { + friend std::ostream& operator<<(std::ostream& out, const ByRef&) { + return out; + } + }; + struct ByValue { + friend std::ostream& operator<<(std::ostream& out, ByValue) { return out; } + }; + struct NoFormatter {}; + )cc")); + EXPECT_THAT( + ir.get_items_if(), + AllOf( + Contains(Pointee(AllOf(RsNameIs("ByRef"), HasDetectedFormatter()))), + Contains(Pointee(AllOf(RsNameIs("ByValue"), HasDetectedFormatter()))), + Contains(Pointee( + AllOf(RsNameIs("NoFormatter"), Not(HasDetectedFormatter())))))); +} + +TEST(ImporterTest, DetectsFormatterAsPrinterOfBase) { + ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCcWithFmt(R"cc( + struct Base { + template + friend void AbslStringify(Sink&, const Base&) {} + }; + struct Derived : Base {}; + )cc")); + EXPECT_THAT( + ir.get_items_if(), + Contains(Pointee(AllOf(RsNameIs("Derived"), HasDetectedFormatter())))); +} + +TEST(ImporterTest, DetectsFormatterAsPrinterInCrtpBase) { + ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCcWithFmt(R"cc( + template + struct Base { + template + friend void AbslStringify(Sink&, const This&) {} + }; + struct Derived : private Base {}; + )cc")); + EXPECT_THAT( + ir.get_items_if(), + Contains(Pointee(AllOf(RsNameIs("Derived"), HasDetectedFormatter())))); +} + +TEST(ImporterTest, DetectsEnumFormatter) { + ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCcWithFmt(R"cc( + enum class Foo { + kFoo, + }; + template + void AbslStringify(Sink&, Foo) {} + )cc")); + EXPECT_THAT( + ir.get_items_if(), + Contains(Pointee(AllOf(RsNameIs("Foo"), HasDetectedFormatter())))); +} + +TEST(ImporterTest, DoesNotDetectAbslStringifyMemberFunctionAsFormatter) { + ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCcWithFmt(R"cc( + struct Foo { + template + static void AbslStringify(Sink&, const Foo&) {} + }; + )cc")); + EXPECT_THAT( + ir.get_items_if(), + Contains(Pointee(AllOf(RsNameIs("Foo"), Not(HasDetectedFormatter()))))); +} + +TEST(ImporterTest, DoesNotDetectOperatorLeftShiftWrongTypesAsFormatter) { + ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCcWithFmt(R"cc( + struct Foo { + friend Foo& operator<<(Foo& foo, int) { return foo; } + }; + )cc")); + EXPECT_THAT( + ir.get_items_if(), + Contains(Pointee(AllOf(RsNameIs("Foo"), Not(HasDetectedFormatter()))))); +} + absl::StatusOr IrFromCcWithAssumedLifetimes(absl::string_view program) { auto full_program = absl::StrCat(R"cc( #define $(l) [[clang::annotate_type("lifetime", #l)]] diff --git a/rs_bindings_from_cc/importers/BUILD b/rs_bindings_from_cc/importers/BUILD index dc128fba1..ddfa90746 100644 --- a/rs_bindings_from_cc/importers/BUILD +++ b/rs_bindings_from_cc/importers/BUILD @@ -54,6 +54,7 @@ cc_library( deps = [ "//lifetime_annotations:type_lifetimes", "//rs_bindings_from_cc:ast_util", + "//rs_bindings_from_cc:bazel_types", "//rs_bindings_from_cc:cc_ir", "//rs_bindings_from_cc:decl_importer", "@abseil-cpp//absl/algorithm:container", diff --git a/rs_bindings_from_cc/importers/cxx_record.cc b/rs_bindings_from_cc/importers/cxx_record.cc index 1e90e30c6..fda56f1d5 100644 --- a/rs_bindings_from_cc/importers/cxx_record.cc +++ b/rs_bindings_from_cc/importers/cxx_record.cc @@ -1110,6 +1110,7 @@ std::optional CXXRecordDeclImporter::Import( FormattedError::FromStatus(std::move(safety_annotation).status())); } + bool fmt_enabled = ictx_.IsFmtEnabledForTarget(owning_target); auto record = Record{ .rs_name = Identifier(rs_name), .cc_name = Identifier(cc_name), @@ -1149,6 +1150,7 @@ std::optional CXXRecordDeclImporter::Import( .child_item_ids = std::move(item_ids), .enclosing_item_id = *std::move(enclosing_item_id), .overloads_operator_delete = MayOverloadOperatorDelete(*record_decl), + .detected_formatter = fmt_enabled && ictx_.DetectFormatter(*record_decl), }; // If the align attribute was attached to the typedef decl, we should diff --git a/rs_bindings_from_cc/importers/enum.cc b/rs_bindings_from_cc/importers/enum.cc index 977e46237..fb7acfef2 100644 --- a/rs_bindings_from_cc/importers/enum.cc +++ b/rs_bindings_from_cc/importers/enum.cc @@ -13,6 +13,7 @@ #include "absl/status/statusor.h" #include "lifetime_annotations/type_lifetimes.h" #include "rs_bindings_from_cc/ast_util.h" +#include "rs_bindings_from_cc/bazel_types.h" #include "rs_bindings_from_cc/ir.h" #include "clang/AST/Decl.h" #include "clang/AST/Type.h" @@ -151,12 +152,14 @@ std::optional EnumDeclImporter::Import(clang::EnumDecl* enum_decl) { } ictx_.MarkAsSuccessfullyImported(enum_decl); + BazelLabel owning_target = ictx_.GetOwningTarget(enum_decl); + bool fmt_enabled = ictx_.IsFmtEnabledForTarget(owning_target); return Enum{ .cc_name = (*enum_name).cc_identifier, .rs_name = (*enum_name).rs_identifier(), .unique_name = ictx_.GetUniqueName(*enum_decl), .id = ictx_.GenerateItemId(enum_decl), - .owning_target = ictx_.GetOwningTarget(enum_decl), + .owning_target = std::move(owning_target), .source_loc = ictx_.ConvertSourceLocation(enum_decl->getBeginLoc()), .underlying_type = *std::move(type), .enumerators = enum_decl->isCompleteDefinition() @@ -164,6 +167,7 @@ std::optional EnumDeclImporter::Import(clang::EnumDecl* enum_decl) { : std::nullopt, .unknown_attr = std::move(*unknown_attr), .enclosing_item_id = *std::move(enclosing_item_id), + .detected_formatter = fmt_enabled && ictx_.DetectFormatter(*enum_decl), }; } diff --git a/rs_bindings_from_cc/ir.cc b/rs_bindings_from_cc/ir.cc index 153384d70..0fd75bd2d 100644 --- a/rs_bindings_from_cc/ir.cc +++ b/rs_bindings_from_cc/ir.cc @@ -674,6 +674,7 @@ llvm::json::Value Record::ToJson() const { {"enclosing_item_id", enclosing_item_id}, {"must_bind", must_bind}, {"overloads_operator_delete", overloads_operator_delete}, + {"detected_formatter", detected_formatter}, }; return llvm::json::Object{ @@ -702,6 +703,7 @@ llvm::json::Value Enum::ToJson() const { {"unknown_attr", unknown_attr}, {"enclosing_item_id", enclosing_item_id}, {"must_bind", must_bind}, + {"detected_formatter", detected_formatter}, }; return llvm::json::Object{ diff --git a/rs_bindings_from_cc/ir.h b/rs_bindings_from_cc/ir.h index a2ed8035e..f7d3c0888 100644 --- a/rs_bindings_from_cc/ir.h +++ b/rs_bindings_from_cc/ir.h @@ -779,6 +779,7 @@ struct Record { std::optional enclosing_item_id; bool must_bind = false; bool overloads_operator_delete = false; + bool detected_formatter = false; }; // A forward-declared record (e.g. `struct Foo;`) @@ -817,6 +818,7 @@ struct Enum { std::optional unknown_attr; std::optional enclosing_item_id; bool must_bind = false; + bool detected_formatter = false; }; struct GlobalVar { diff --git a/rs_bindings_from_cc/ir.rs b/rs_bindings_from_cc/ir.rs index aa652cb60..bfce36cf7 100644 --- a/rs_bindings_from_cc/ir.rs +++ b/rs_bindings_from_cc/ir.rs @@ -1249,6 +1249,7 @@ pub struct Record { // Lifetime variable names bound by this record. #[serde(default)] pub lifetime_inputs: Vec>, + pub detected_formatter: bool, } impl GenericItem for Record { @@ -1453,6 +1454,7 @@ pub struct Enum { pub unknown_attr: Option>, pub enclosing_item_id: Option, pub must_bind: bool, + pub detected_formatter: bool, } impl GenericItem for Enum { @@ -2311,6 +2313,13 @@ impl IR { }) } + pub fn enums(&self) -> impl Iterator> { + self.items().filter_map(|item| match item { + Item::Enum(enum_item) => Some(enum_item), + _ => None, + }) + } + pub fn unsupported_items(&self) -> impl Iterator> { self.items().filter_map(|item| match item { Item::UnsupportedItem(unsupported_item) => Some(unsupported_item), diff --git a/rs_bindings_from_cc/ir_from_cc_test.rs b/rs_bindings_from_cc/ir_from_cc_test.rs index 50f114e65..260c0992e 100644 --- a/rs_bindings_from_cc/ir_from_cc_test.rs +++ b/rs_bindings_from_cc/ir_from_cc_test.rs @@ -41,6 +41,15 @@ fn ir_from_assumed_lifetimes_cc(program: &str) -> Result { ) } +fn ir_from_fmt_cc(program: &str) -> Result { + ir_testing::ir_from_cc_dependency( + multiplatform_testing::test_platform(), + program, + "// empty header", + Some("fmt"), + ) +} + #[gtest] fn test_function() { let ir = ir_from_cc("int f(int a, int b);").unwrap(); @@ -4553,3 +4562,29 @@ fn test_assumed_lifetimes_lifetime_capture_by_multiple_params() { } ); } + +#[gtest] +fn test_detects_formatter() { + let ir = ir_from_fmt_cc( + r#" + struct Foo { + template + friend void AbslStringify(Sink&, const Foo&) {} + };"#, + ) + .unwrap(); + assert_ir_matches!( + ir, + quote! { + ... + Record { + ... + cc_name: "Foo", + ... + detected_formatter: true, + ... + } + ... + } + ); +}