| // 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 <optional> |
| #include <string> |
| #include <variant> |
| #include <vector> |
| |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| #include "absl/status/status.h" |
| #include "absl/strings/match.h" |
| #include "absl/strings/string_view.h" |
| #include "common/status_test_matchers.h" |
| #include "rs_bindings_from_cc/bazel_types.h" |
| #include "rs_bindings_from_cc/ir.h" |
| #include "rs_bindings_from_cc/ir_from_cc.h" |
| |
| namespace crubit { |
| namespace { |
| |
| using ::testing::AllOf; |
| using ::testing::AnyOf; |
| using ::testing::Contains; |
| using ::testing::Each; |
| using ::testing::ElementsAre; |
| using ::testing::HasSubstr; |
| using ::testing::IsEmpty; |
| using ::testing::Not; |
| using ::testing::Pointee; |
| using ::testing::SizeIs; |
| using ::testing::UnorderedElementsAre; |
| using ::testing::VariantWith; |
| |
| std::optional<ItemId> DeclIdForRecord(const IR& ir, absl::string_view rs_name) { |
| for (const Record* record : ir.get_items_if<Record>()) { |
| if (record->rs_name == rs_name) { |
| return record->id; |
| } |
| } |
| return std::nullopt; |
| } |
| |
| std::optional<IR::Item> FindItemById(const IR& ir, ItemId id) { |
| for (auto item : ir.items) { |
| if (auto* record = std::get_if<Record>(&item); record && record->id == id) { |
| return item; |
| } else if (auto* func = std::get_if<Func>(&item); func && func->id == id) { |
| return item; |
| } else if (auto* comment = std::get_if<Comment>(&item); |
| comment && comment->id == id) { |
| return item; |
| } else if (auto* unsupported = std::get_if<UnsupportedItem>(&item); |
| unsupported && unsupported->id == id) { |
| return item; |
| } else if (auto* ns = std::get_if<Namespace>(&item); ns && ns->id == id) { |
| return item; |
| } else if (auto* incomplete = std::get_if<IncompleteRecord>(&item); |
| incomplete && incomplete->id == id) { |
| return item; |
| } |
| } |
| return std::nullopt; |
| } |
| |
| template <typename T> |
| UnqualifiedIdentifier GetName(const T& x) { |
| return x.identifier; |
| } |
| UnqualifiedIdentifier GetName(const Func& x) { return x.name; } |
| UnqualifiedIdentifier GetName(const Namespace& x) { return x.name; } |
| |
| // Matches an IR node that has the given identifier. |
| MATCHER_P(IdentifierIs, identifier, "") { |
| UnqualifiedIdentifier name = GetName(arg); |
| const Identifier* actual = std::get_if<Identifier>(&name); |
| if (actual == nullptr) { |
| *result_listener << "actual name not an identifier."; |
| return false; |
| } |
| if (actual->Ident() == identifier) return true; |
| |
| *result_listener << "actual identifier: '" << actual->Ident() << "'"; |
| return false; |
| } |
| |
| // Matches an Record node that has the given `rs_name`. |
| MATCHER_P(RsNameIs, rs_name, "") { return arg.rs_name == rs_name; } |
| |
| // Matches an IR node that has the given doc comment. |
| MATCHER_P(DocCommentIs, doc_comment, "") { |
| if (arg.doc_comment && *arg.doc_comment == doc_comment) return true; |
| |
| *result_listener << "actual doc comment: '" |
| << (arg.doc_comment ? *arg.doc_comment : "<none>") << "'"; |
| return false; |
| } |
| |
| // Matches a Func that has the given mangled name. |
| MATCHER_P(MangledNameIs, mangled_name, "") { |
| if (arg.mangled_name == mangled_name) return true; |
| |
| *result_listener << "actual mangled name: '" << arg.mangled_name << "'"; |
| return false; |
| } |
| |
| // Matches a Func that has a return type matching `matcher`. |
| template <typename Matcher> |
| auto ReturnType(const Matcher& matcher) { |
| return testing::Field("return_type", &Func::return_type, matcher); |
| } |
| |
| // Matches a Func that has parameters matching `matchers`. |
| template <typename... Args> |
| auto ParamsAre(const Args&... matchers) { |
| return testing::Field("params", &Func::params, ElementsAre(matchers...)); |
| } |
| |
| // Matches a Func that is inline. |
| MATCHER(IsInline, "") { return arg.is_inline; } |
| |
| // Matches a FuncParam with a type that matches all given matchers. |
| template <typename... Args> |
| auto ParamType(const Args&... matchers) { |
| return testing::Field("type", &FuncParam::type, AllOf(matchers...)); |
| } |
| |
| // Matches an RsType or CcType that has the given name. |
| MATCHER_P(NameIs, name, "") { |
| if (arg.name == name) return true; |
| |
| *result_listener << "actual name: '" << arg.name << "'"; |
| return false; |
| } |
| |
| // Matches text for comments. |
| MATCHER_P(TextIs, text, "") { |
| if (arg.text == text) return true; |
| |
| *result_listener << "actual text: '" << arg.text << "'"; |
| return false; |
| } |
| |
| // Matches an RsType or CcType that has the given decl_id. |
| MATCHER_P(DeclIdIs, decl_id, "") { |
| if (arg.decl_id.has_value() && *arg.decl_id == decl_id) return true; |
| |
| *result_listener << "actual decl_id: "; |
| if (arg.decl_id.has_value()) { |
| *result_listener << *arg.decl_id; |
| } else { |
| *result_listener << "std::nullopt"; |
| } |
| return false; |
| } |
| |
| // Matches an RsType or CcType that is const . |
| MATCHER(IsConst, "") { return arg.is_const; } |
| |
| // Matches a MappedType with a CcType that matches all given matchers. |
| template <typename... Args> |
| auto CcTypeIs(const Args&... matchers) { |
| return testing::Field("cpp_type", &MappedType::cpp_type, AllOf(matchers...)); |
| } |
| |
| // Matches a MappedType with a RsType that matches all given matchers. |
| template <typename... Args> |
| auto RsTypeIs(const Args&... matchers) { |
| return testing::Field("rs_type", &MappedType::rs_type, AllOf(matchers...)); |
| } |
| |
| // Matches an RsType that has type arguments matching `matchers`. |
| template <typename... Args> |
| auto RsTypeParamsAre(const Args&... matchers) { |
| return testing::Field("type_args", &RsType::type_args, |
| ElementsAre(matchers...)); |
| } |
| |
| // Matches a CcType that has type arguments matching `matchers`. |
| template <typename... Args> |
| auto CcTypeParamsAre(const Args&... matchers) { |
| return testing::Field("type_args", &CcType::type_args, |
| ElementsAre(matchers...)); |
| } |
| |
| auto IsCcInt() { return AllOf(NameIs("int"), CcTypeParamsAre()); } |
| |
| auto IsRsInt() { |
| return AllOf(NameIs("::core::ffi::c_int"), RsTypeParamsAre()); |
| } |
| |
| // Matches a CcType that is a pointer to a type matching `matcher`. |
| template <typename Matcher> |
| auto CcPointsTo(const Matcher& matcher) { |
| return AllOf(NameIs("*"), CcTypeParamsAre(matcher)); |
| } |
| |
| template <typename Matcher> |
| auto CcReferenceTo(const Matcher& matcher) { |
| return AllOf(NameIs("&"), CcTypeParamsAre(matcher)); |
| } |
| |
| // Matches an RsType that is a mutable pointer to a type matching `matcher`. |
| template <typename Matcher> |
| auto RsPointsTo(const Matcher& matcher) { |
| return AllOf(NameIs("*mut"), RsTypeParamsAre(matcher)); |
| } |
| |
| // Matches an RsType that is a const pointer to a type matching `matcher`. |
| template <typename Matcher> |
| auto RsConstPointsTo(const Matcher& matcher) { |
| return AllOf(NameIs("*const"), RsTypeParamsAre(matcher)); |
| } |
| |
| // Matches a MappedType that is void. |
| MATCHER(IsVoid, "") { return arg.IsVoid(); } |
| |
| // Matches a MappedType that is a pointer to integer. |
| auto IsIntPtr() { |
| return AllOf(CcTypeIs(CcPointsTo(IsCcInt())), |
| RsTypeIs(RsPointsTo(IsRsInt()))); |
| } |
| |
| // Matches a MappedType that is an lvalue reference to integer. |
| auto IsIntRef() { |
| return AllOf(CcTypeIs(CcReferenceTo(IsCcInt())), |
| RsTypeIs(RsPointsTo(IsRsInt()))); |
| } |
| |
| // Matches a Record that has fields matching `matchers`. |
| template <typename... Args> |
| auto FieldsAre(const Args&... matchers) { |
| return testing::Field("fields", &Record::fields, ElementsAre(matchers...)); |
| } |
| |
| // Matches a Record that has the given size. |
| MATCHER_P(RecordSizeIs, size, "") { |
| if (arg.size == size) return true; |
| |
| *result_listener << "actual size: " << arg.size; |
| return false; |
| } |
| |
| // Matches a Record that has the given alignment. |
| MATCHER_P(AlignmentIs, alignment, "") { |
| if (arg.alignment == alignment) return true; |
| |
| *result_listener << "actual alignment: " << arg.alignment; |
| return false; |
| } |
| |
| // Matches a Record with a copy_constructor that matches all given matchers. |
| template <typename... Args> |
| auto CopyConstructor(const Args&... matchers) { |
| return testing::Field("copy_constructor", &Record::copy_constructor, |
| AllOf(matchers...)); |
| } |
| |
| // Matches a Record with a move_constructor that matches all given matchers. |
| template <typename... Args> |
| auto MoveConstructor(const Args&... matchers) { |
| return testing::Field("move_constructor", &Record::move_constructor, |
| AllOf(matchers...)); |
| } |
| |
| // Matches a Record with a destructor that matches all given matchers. |
| template <typename... Args> |
| auto Destructor(const Args&... matchers) { |
| return testing::Field("destructor", &Record::destructor, AllOf(matchers...)); |
| } |
| |
| // Matches a Record which is trivial for calls. |
| MATCHER(IsTrivialAbi, "") { return arg.is_trivial_abi; } |
| |
| // Matches a Field that has the given offset. |
| MATCHER_P(OffsetIs, offset, "") { |
| if (arg.offset == offset) return true; |
| |
| *result_listener << "actual offset: " << arg.offset; |
| return false; |
| } |
| |
| // Matches a Field with a type that matches all given matchers. |
| template <typename... Args> |
| auto FieldType(const Args&... matchers) { |
| return testing::Field("type", &Field::type, AllOf(matchers...)); |
| } |
| |
| // Return the items from `ir` without predefined builtin types. |
| decltype(IR::items) ItemsWithoutBuiltins(const IR& ir) { |
| decltype(IR::items) items; |
| |
| for (const auto& item : ir.items) { |
| if (const auto* type_alias = std::get_if<TypeAlias>(&item)) { |
| if (type_alias->identifier.Ident() == "__builtin_ms_va_list") { |
| continue; |
| } |
| } |
| items.push_back(item); |
| } |
| |
| return items; |
| } |
| |
| TEST(ImporterTest, Noop) { |
| // Nothing interesting there, but also not empty, so that the header gets |
| // generated. |
| ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({" "})); |
| |
| EXPECT_THAT(ItemsWithoutBuiltins(ir), IsEmpty()); |
| } |
| |
| TEST(ImporterTest, ErrorOnInvalidInput) { |
| ASSERT_THAT(IrFromCc({"int foo(); But this is not C++"}), |
| StatusIs(absl::StatusCode::kInvalidArgument)); |
| } |
| |
| TEST(ImporterTest, FuncWithVoidReturnType) { |
| ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({"void Foo();"})); |
| EXPECT_THAT(ItemsWithoutBuiltins(ir), |
| UnorderedElementsAre(VariantWith<Func>( |
| AllOf(IdentifierIs("Foo"), MangledNameIs("_Z3Foov"), |
| ReturnType(IsVoid()), ParamsAre())))); |
| } |
| |
| TEST(ImporterTest, TwoFuncs) { |
| ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({"void Foo(); void Bar();"})); |
| EXPECT_THAT( |
| ItemsWithoutBuiltins(ir), |
| UnorderedElementsAre( |
| VariantWith<Func>(AllOf(IdentifierIs("Foo"), MangledNameIs("_Z3Foov"), |
| ReturnType(IsVoid()), ParamsAre())), |
| VariantWith<Func>(AllOf(IdentifierIs("Bar"), MangledNameIs("_Z3Barv"), |
| ReturnType(IsVoid()), ParamsAre())))); |
| } |
| |
| TEST(ImporterTest, TwoFuncsFromTwoHeaders) { |
| ASSERT_OK_AND_ASSIGN( |
| IR ir, |
| IrFromCc({.current_target = BazelLabel{"//two_funcs:one_target"}, |
| .public_headers = {HeaderName("test/testing_header_0.h"), |
| HeaderName("test/testing_header_1.h")}, |
| .virtual_headers_contents_for_testing = |
| {{HeaderName("test/testing_header_0.h"), "void Foo();"}, |
| {HeaderName("test/testing_header_1.h"), "void Bar();"}}, |
| .headers_to_targets = { |
| {HeaderName("test/testing_header_0.h"), |
| BazelLabel{"//two_funcs:one_target"}}, |
| {HeaderName("test/testing_header_1.h"), |
| BazelLabel{"//two_funcs:one_target"}}, |
| }})); |
| EXPECT_THAT(ItemsWithoutBuiltins(ir), |
| UnorderedElementsAre(VariantWith<Func>(IdentifierIs("Foo")), |
| VariantWith<Func>(IdentifierIs("Bar")))); |
| } |
| |
| TEST(ImporterTest, NonInlineFunc) { |
| ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({"void Foo() {}"})); |
| EXPECT_THAT(ItemsWithoutBuiltins(ir), |
| UnorderedElementsAre(VariantWith<Func>( |
| AllOf(IdentifierIs("Foo"), Not(IsInline()))))); |
| } |
| |
| TEST(ImporterTest, InlineFunc) { |
| ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({"inline void Foo() {}"})); |
| EXPECT_THAT(ItemsWithoutBuiltins(ir), |
| UnorderedElementsAre( |
| VariantWith<Func>(AllOf(IdentifierIs("Foo"), IsInline())))); |
| } |
| |
| TEST(ImporterTest, FuncJustOnce) { |
| ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({"void Foo(); void Foo();"})); |
| EXPECT_THAT( |
| ItemsWithoutBuiltins(ir), |
| UnorderedElementsAre(VariantWith<Func>(AllOf(IdentifierIs("Foo"))))); |
| } |
| |
| TEST(ImporterTest, TestImportPointerFunc) { |
| ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({"int* Foo(int* a);"})); |
| |
| EXPECT_THAT(ItemsWithoutBuiltins(ir), |
| UnorderedElementsAre(VariantWith<Func>(AllOf( |
| ReturnType(IsIntPtr()), ParamsAre(ParamType(IsIntPtr())))))); |
| } |
| |
| TEST(ImporterTest, TestImportConstStructPointerFunc) { |
| ASSERT_OK_AND_ASSIGN(IR ir, |
| IrFromCc({"struct S{}; const S* Foo(const S* s);"})); |
| |
| std::optional<ItemId> decl_id = DeclIdForRecord(ir, "S"); |
| ASSERT_TRUE(decl_id.has_value()); |
| |
| auto is_ptr_to_const_s = |
| AllOf(CcTypeIs(CcPointsTo(AllOf(DeclIdIs(*decl_id), IsConst()))), |
| RsTypeIs(RsConstPointsTo(DeclIdIs(*decl_id)))); |
| |
| EXPECT_THAT(ir.items, Contains(VariantWith<Func>(AllOf( |
| IdentifierIs("Foo"), ReturnType(is_ptr_to_const_s), |
| ParamsAre(ParamType(is_ptr_to_const_s)))))); |
| } |
| |
| TEST(ImporterTest, TestImportReferenceFunc) { |
| ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({"int& Foo(int& a);"})); |
| |
| EXPECT_THAT(ItemsWithoutBuiltins(ir), |
| UnorderedElementsAre(VariantWith<Func>(AllOf( |
| ReturnType(IsIntRef()), ParamsAre(ParamType(IsIntRef())))))); |
| } |
| |
| TEST(ImporterTest, TrivialCopyConstructor) { |
| absl::string_view file = R"cc( |
| struct Implicit {}; |
| struct Defaulted { |
| Defaulted(const Defaulted&) = default; |
| }; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCc({file})); |
| |
| std::vector<const Record*> records = ir.get_items_if<Record>(); |
| EXPECT_THAT(records, SizeIs(2)); |
| EXPECT_THAT(records, |
| Each(Pointee(CopyConstructor(SpecialMemberFunc::kTrivial)))); |
| } |
| |
| TEST(ImporterTest, NontrivialUserDefinedCopyConstructor) { |
| absl::string_view file = R"cc( |
| struct NontrivialUserDefined { |
| NontrivialUserDefined(const NontrivialUserDefined&); |
| }; |
| struct NontrivialSub : public NontrivialUserDefined {}; |
| |
| // Despite having a defaulted copy constructor, this is not trivially |
| // copyable, because the *first* declaration is not defaulted. |
| struct NontrivialUserDefinedDefaulted { |
| NontrivialUserDefinedDefaulted(const NontrivialUserDefinedDefaulted&); |
| }; |
| inline NontrivialUserDefinedDefaulted::NontrivialUserDefinedDefaulted( |
| const NontrivialUserDefinedDefaulted&) = default; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCc({file})); |
| |
| std::vector<const Record*> records = ir.get_items_if<Record>(); |
| EXPECT_THAT(records, SizeIs(3)); |
| EXPECT_THAT(records, Each(Pointee(CopyConstructor( |
| SpecialMemberFunc::kNontrivialUserDefined)))); |
| } |
| |
| TEST(ImporterTest, NontrivialMembersCopyConstructor) { |
| absl::string_view file = R"cc( |
| struct NontrivialUserDefined { |
| NontrivialUserDefined(const NontrivialUserDefined&); |
| }; |
| struct MemberImplicit { |
| NontrivialUserDefined x; |
| }; |
| struct MemberDefaulted { |
| MemberDefaulted(const MemberDefaulted&) = default; |
| NontrivialUserDefined x; |
| }; |
| struct Subclass : public MemberImplicit {}; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCc({file})); |
| std::vector<const Record*> records = ir.get_items_if<Record>(); |
| EXPECT_THAT(records, SizeIs(4)); |
| EXPECT_THAT( |
| records, |
| Each(Pointee(AnyOf( |
| RsNameIs( |
| "NontrivialUserDefined"), // needed to create nontrivial members |
| CopyConstructor(SpecialMemberFunc::kNontrivialMembers))))); |
| } |
| |
| TEST(ImporterTest, DeletedCopyConstructor) { |
| absl::string_view file = R"cc( |
| struct Deleted { |
| Deleted(const Deleted&) = delete; |
| }; |
| struct DeletedByMember { |
| Deleted x; |
| }; |
| struct DeletedByCtorDef { |
| DeletedByCtorDef(DeletedByCtorDef&&) {} |
| }; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCc({file})); |
| std::vector<const Record*> records = ir.get_items_if<Record>(); |
| EXPECT_THAT(records, SizeIs(3)); |
| EXPECT_THAT(records, |
| Each(Pointee(CopyConstructor(SpecialMemberFunc::kUnavailable)))); |
| } |
| |
| TEST(ImporterTest, PublicCopyConstructor) { |
| absl::string_view file = R"cc( |
| class Implicit {}; |
| struct Defaulted { |
| Defaulted(const Defaulted&) = default; |
| }; |
| class Section { |
| public: |
| Section(const Section&) = default; |
| }; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCc({file})); |
| |
| std::vector<const Record*> records = ir.get_items_if<Record>(); |
| EXPECT_THAT(records, SizeIs(3)); |
| EXPECT_THAT(records, |
| Each(Pointee(CopyConstructor(SpecialMemberFunc::kTrivial)))); |
| } |
| |
| TEST(ImporterTest, PrivateCopyConstructor) { |
| absl::string_view file = R"cc( |
| class Defaulted { |
| Defaulted(const Defaulted&) = default; |
| }; |
| struct Section { |
| private: |
| Section(const Section&) = default; |
| }; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCc({file})); |
| |
| std::vector<const Record*> records = ir.get_items_if<Record>(); |
| EXPECT_THAT(records, SizeIs(2)); |
| EXPECT_THAT(records, |
| Each(Pointee(CopyConstructor(SpecialMemberFunc::kUnavailable)))); |
| } |
| |
| TEST(ImporterTest, TrivialMoveConstructor) { |
| absl::string_view file = R"cc( |
| struct Implicit {}; |
| struct Defaulted { |
| Defaulted(Defaulted&&) = default; |
| }; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCc({file})); |
| |
| std::vector<const Record*> records = ir.get_items_if<Record>(); |
| EXPECT_THAT(records, SizeIs(2)); |
| EXPECT_THAT(records, |
| Each(Pointee(MoveConstructor(SpecialMemberFunc::kTrivial)))); |
| } |
| |
| TEST(ImporterTest, NontrivialUserDefinedMoveConstructor) { |
| absl::string_view file = R"cc( |
| struct NontrivialUserDefined { |
| NontrivialUserDefined(NontrivialUserDefined&&); |
| }; |
| struct NontrivialSub : public NontrivialUserDefined {}; |
| |
| // Despite having a defaulted move constructor, this is not trivially |
| // movable, because the *first* declaration is not defaulted. |
| struct NontrivialUserDefinedDefaulted { |
| NontrivialUserDefinedDefaulted(NontrivialUserDefinedDefaulted&&); |
| }; |
| inline NontrivialUserDefinedDefaulted::NontrivialUserDefinedDefaulted( |
| NontrivialUserDefinedDefaulted&&) = default; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCc({file})); |
| std::vector<const Record*> records = ir.get_items_if<Record>(); |
| EXPECT_THAT(records, SizeIs(3)); |
| EXPECT_THAT(records, Each(Pointee(MoveConstructor( |
| SpecialMemberFunc::kNontrivialUserDefined)))); |
| } |
| |
| TEST(ImporterTest, NontrivialMembersMoveConstructor) { |
| absl::string_view file = R"cc( |
| struct NontrivialUserDefined { |
| NontrivialUserDefined(NontrivialUserDefined&&); |
| }; |
| struct MemberImplicit { |
| NontrivialUserDefined x; |
| }; |
| struct MemberDefaulted { |
| MemberDefaulted(MemberDefaulted&&) = default; |
| NontrivialUserDefined x; |
| }; |
| struct Subclass : public MemberImplicit {}; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCc({file})); |
| std::vector<const Record*> records = ir.get_items_if<Record>(); |
| EXPECT_THAT(records, SizeIs(4)); |
| EXPECT_THAT( |
| records, |
| Each(Pointee(AnyOf( |
| RsNameIs( |
| "NontrivialUserDefined"), // needed to create nontrivial members |
| MoveConstructor(SpecialMemberFunc::kNontrivialMembers))))); |
| } |
| |
| TEST(ImporterTest, DeletedMoveConstructor) { |
| absl::string_view file = R"cc( |
| struct Deleted { |
| Deleted(Deleted&&) = delete; |
| }; |
| struct DeletedByMember { |
| Deleted x; |
| }; |
| struct SuppressedByCtorDef { |
| SuppressedByCtorDef(const SuppressedByCtorDef&) {} |
| }; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCc({file})); |
| std::vector<const Record*> records = ir.get_items_if<Record>(); |
| EXPECT_THAT(records, SizeIs(3)); |
| EXPECT_THAT(records, |
| Each(Pointee(MoveConstructor(SpecialMemberFunc::kUnavailable)))); |
| } |
| |
| TEST(ImporterTest, PublicMoveConstructor) { |
| absl::string_view file = R"cc( |
| class Implicit {}; |
| struct Defaulted { |
| Defaulted(Defaulted&&) = default; |
| }; |
| class Section { |
| public: |
| Section(Section&&) = default; |
| }; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCc({file})); |
| |
| std::vector<const Record*> records = ir.get_items_if<Record>(); |
| EXPECT_THAT(records, SizeIs(3)); |
| EXPECT_THAT(records, |
| Each(Pointee(MoveConstructor(SpecialMemberFunc::kTrivial)))); |
| } |
| |
| TEST(ImporterTest, PrivateMoveConstructor) { |
| absl::string_view file = R"cc( |
| class Defaulted { |
| Defaulted(Defaulted&&) = default; |
| }; |
| struct Section { |
| private: |
| Section(Section&&) = default; |
| }; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCc({file})); |
| |
| std::vector<const Record*> records = ir.get_items_if<Record>(); |
| EXPECT_THAT(records, SizeIs(2)); |
| EXPECT_THAT(records, |
| Each(Pointee(MoveConstructor(SpecialMemberFunc::kUnavailable)))); |
| } |
| |
| TEST(ImporterTest, TrivialDestructor) { |
| absl::string_view file = R"cc( |
| struct Implicit {}; |
| struct Defaulted { |
| ~Defaulted() = default; |
| }; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCc({file})); |
| |
| std::vector<const Record*> records = ir.get_items_if<Record>(); |
| EXPECT_THAT(records, SizeIs(2)); |
| EXPECT_THAT(records, Each(Pointee(Destructor(SpecialMemberFunc::kTrivial)))); |
| } |
| |
| TEST(ImporterTest, NontrivialUserDefinedDestructor) { |
| absl::string_view file = R"cc( |
| struct NontrivialUserDefined { |
| ~NontrivialUserDefined(); |
| }; |
| struct NontrivialSub : public NontrivialUserDefined {}; |
| |
| // Despite having a defaulted destructor, this is not trivially |
| // destructible, because the destructor is virtual. |
| struct VirtualDestructor { |
| virtual ~VirtualDestructor() = default; |
| }; |
| |
| // Despite having a defaulted destructor, this is not trivially |
| // destructible, because the *first* declaration is not defaulted. |
| struct NontrivialUserDefinedDefaulted { |
| ~NontrivialUserDefinedDefaulted(); |
| }; |
| inline NontrivialUserDefinedDefaulted::~NontrivialUserDefinedDefaulted() = |
| default; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCc({file})); |
| std::vector<const Record*> records = ir.get_items_if<Record>(); |
| EXPECT_THAT(records, SizeIs(4)); |
| EXPECT_THAT( |
| records, |
| Each(Pointee(Destructor(SpecialMemberFunc::kNontrivialUserDefined)))); |
| } |
| |
| TEST(ImporterTest, NontrivialMembersDestructor) { |
| absl::string_view file = R"cc( |
| struct NontrivialUserDefined { |
| ~NontrivialUserDefined(); |
| }; |
| struct MemberImplicit { |
| NontrivialUserDefined x; |
| }; |
| struct MemberDefaulted { |
| MemberDefaulted(MemberDefaulted&&) = default; |
| NontrivialUserDefined x; |
| }; |
| struct Subclass : public MemberImplicit {}; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCc({file})); |
| std::vector<const Record*> records = ir.get_items_if<Record>(); |
| EXPECT_THAT(records, SizeIs(4)); |
| EXPECT_THAT( |
| records, |
| Each(Pointee(AnyOf( |
| RsNameIs( |
| "NontrivialUserDefined"), // needed to create nontrivial members |
| Destructor(SpecialMemberFunc::kNontrivialMembers))))); |
| } |
| |
| TEST(ImporterTest, DeletedDestructor) { |
| absl::string_view file = R"cc( |
| struct Deleted { |
| ~Deleted() = delete; |
| }; |
| struct DeletedByMember { |
| Deleted x; |
| }; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCc({file})); |
| |
| std::vector<const Record*> records = ir.get_items_if<Record>(); |
| EXPECT_THAT(records, SizeIs(2)); |
| EXPECT_THAT(records, |
| Each(Pointee(Destructor(SpecialMemberFunc::kUnavailable)))); |
| } |
| |
| TEST(ImporterTest, PublicDestructor) { |
| absl::string_view file = R"cc( |
| class Implicit {}; |
| struct Defaulted { |
| ~Defaulted() = default; |
| }; |
| class Section { |
| public: |
| ~Section() = default; |
| }; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCc({file})); |
| |
| std::vector<const Record*> records = ir.get_items_if<Record>(); |
| EXPECT_THAT(records, SizeIs(3)); |
| EXPECT_THAT(records, Each(Pointee(Destructor(SpecialMemberFunc::kTrivial)))); |
| } |
| |
| TEST(ImporterTest, PrivateDestructor) { |
| absl::string_view file = R"cc( |
| class Defaulted { |
| ~Defaulted() = default; |
| }; |
| struct Section { |
| private: |
| ~Section() = default; |
| }; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCc({file})); |
| |
| std::vector<const Record*> records = ir.get_items_if<Record>(); |
| EXPECT_THAT(records, SizeIs(2)); |
| EXPECT_THAT(records, |
| Each(Pointee(Destructor(SpecialMemberFunc::kUnavailable)))); |
| } |
| |
| TEST(ImporterTest, TrivialAbi) { |
| absl::string_view file = R"cc( |
| struct Empty {}; |
| struct Defaulted { |
| Defaulted(const Defaulted&) = default; |
| }; |
| struct [[clang::trivial_abi]] Nontrivial { |
| Nontrivial(const Nontrivial&) {} |
| }; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCc({file})); |
| |
| std::vector<const Record*> records = ir.get_items_if<Record>(); |
| EXPECT_THAT(records, SizeIs(3)); |
| EXPECT_THAT(records, Each(Pointee(IsTrivialAbi()))); |
| } |
| |
| TEST(ImporterTest, NotTrivialAbi) { |
| absl::string_view file = R"cc( |
| struct Nontrivial { |
| Nontrivial(const Nontrivial&) {} |
| }; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCc({file})); |
| |
| std::vector<const Record*> records = ir.get_items_if<Record>(); |
| EXPECT_THAT(records, SizeIs(1)); |
| EXPECT_THAT(records, Each(Pointee(Not(IsTrivialAbi())))); |
| } |
| |
| TEST(ImporterTest, TopLevelItemIds) { |
| absl::string_view file = R"cc( |
| struct ForwardDeclaration; |
| struct TopLevelStruct {}; |
| // Top level comment |
| |
| // Function comment |
| void top_level_func(); |
| namespace top_level_namespace { |
| struct Nested {}; |
| // free nested comment |
| |
| // nested_func comment |
| void nested_func(); |
| } // namespace top_level_namespace |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file})); |
| |
| std::vector<IR::Item> items; |
| for (const auto& id : ir.top_level_item_ids) { |
| auto item = FindItemById(ir, id); |
| ASSERT_TRUE(item.has_value()); |
| items.push_back(*item); |
| } |
| |
| EXPECT_THAT( |
| items, |
| ElementsAre( |
| VariantWith<IncompleteRecord>(RsNameIs("ForwardDeclaration")), |
| VariantWith<Record>(RsNameIs("TopLevelStruct")), |
| VariantWith<Comment>(TextIs("Top level comment")), |
| VariantWith<Func>(IdentifierIs("top_level_func")), |
| VariantWith<Namespace>(IdentifierIs("top_level_namespace")), |
| VariantWith<Comment>(TextIs("namespace top_level_namespace")))); |
| } |
| |
| TEST(ImporterTest, ForwardDeclarationAndDefinition) { |
| absl::string_view file = R"cc( |
| struct ForwardDeclaredStruct; |
| struct ForwardDeclaredStruct {}; |
| struct Struct {}; |
| struct Struct; |
| struct ForwardDeclaredStructWithNoDefinition; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file})); |
| |
| std::vector<IR::Item> items; |
| for (const auto& id : ir.top_level_item_ids) { |
| auto item = FindItemById(ir, id); |
| items.push_back(*item); |
| } |
| |
| EXPECT_THAT( |
| items, ElementsAre(VariantWith<Record>(RsNameIs("ForwardDeclaredStruct")), |
| VariantWith<Record>(RsNameIs("Struct")), |
| VariantWith<IncompleteRecord>(RsNameIs( |
| "ForwardDeclaredStructWithNoDefinition")))); |
| } |
| |
| TEST(ImporterTest, DuplicateForwardDeclarations) { |
| absl::string_view file = R"cc( |
| struct ForwardDeclaredStructWithNoDefinition; |
| struct ForwardDeclaredStructWithNoDefinition; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file})); |
| |
| std::vector<IR::Item> items; |
| for (const auto& id : ir.top_level_item_ids) { |
| auto item = FindItemById(ir, id); |
| items.push_back(*item); |
| } |
| |
| EXPECT_THAT(items, ElementsAre(VariantWith<IncompleteRecord>( |
| RsNameIs("ForwardDeclaredStructWithNoDefinition")))); |
| } |
| |
| TEST(ImporterTest, RecordItemIds) { |
| absl::string_view file = R"cc( |
| struct TopLevelStruct { |
| // A free comment |
| |
| // foo comment |
| int foo; |
| |
| int bar(); |
| struct Nested {}; |
| int baz(); |
| }; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(const IR ir, IrFromCc({file})); |
| |
| std::vector<const Record*> records = ir.get_items_if<Record>(); |
| ASSERT_EQ(records.size(), 1); |
| |
| std::vector<IR::Item> items; |
| for (const auto& id : records[0]->child_item_ids) { |
| auto item = FindItemById(ir, id); |
| ASSERT_TRUE(item.has_value()); |
| items.push_back(*item); |
| } |
| |
| EXPECT_THAT(items, |
| AllOf(Contains(VariantWith<Comment>(TextIs("A free comment"))), |
| Contains(VariantWith<Func>(IdentifierIs("bar"))), |
| Contains(VariantWith<UnsupportedItem>( |
| NameIs("TopLevelStruct::Nested"))), |
| Contains(VariantWith<Func>(IdentifierIs("baz"))))); |
| } |
| |
| TEST(ImporterTest, FailedClassTemplateMethod) { |
| absl::string_view file = R"cc( |
| struct NoMethod final {}; |
| template <typename T> |
| struct A final { |
| auto CallMethod(T t) { return t.method(); } |
| }; |
| using B = A<NoMethod>; |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file})); |
| |
| const UnsupportedItem* unsupported_method = nullptr; |
| for (auto unsupported_item : ir.get_items_if<UnsupportedItem>()) { |
| if (unsupported_item->name == "A<NoMethod>::CallMethod") { |
| unsupported_method = unsupported_item; |
| break; |
| } |
| } |
| ASSERT_TRUE(unsupported_method != nullptr); |
| EXPECT_THAT( |
| unsupported_method->errors, |
| Contains(testing::Property( |
| "message", &FormattedError::message, |
| HasSubstr( |
| // clang-format off |
| R"(Diagnostics emitted: |
| ir_from_cc_virtual_header.h:5:12: note: in instantiation of member function 'A<NoMethod>::CallMethod' requested here |
| ir_from_cc_virtual_header.h:5:39: error: no member named 'method' in 'NoMethod')") |
| // clang-format on |
| ))); |
| } |
| |
| TEST(ImporterTest, CrashRepro_FunctionTypeAlias) { |
| absl::string_view file = R"cc( |
| using Callback = void(const int&); |
| void SetHook(Callback* cb); |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file})); |
| } |
| |
| TEST(ImporterTest, CrashRepro_DecltypeInvolvingTemplate) { |
| absl::string_view file = R"cc( |
| template <class T> |
| struct A {}; |
| struct B { |
| A<int> a; |
| }; |
| template <class Trait> |
| struct C { |
| static decltype(Trait::a) Func(); |
| }; |
| // Note that to trigger the crash, we specifically require the following: |
| // - `C::Func()` needs to be static. |
| // - We need to call `C` function on a variable `c` (we don't crash if we |
| // call `C::Func()`. |
| // - `c` needs to be a parameter (we don't crash if it is a local variable). |
| void Func(C<B> c) { c.Func(); } |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file})); |
| } |
| |
| TEST(ImporterTest, CrashRepro_AutoInvolvingTemplate) { |
| absl::string_view file = R"cc( |
| template <class T> |
| struct Template {}; |
| auto Func() { return Template<int>{}; } |
| )cc"; |
| ASSERT_OK_AND_ASSIGN(IR ir, IrFromCc({file})); |
| } |
| |
| } // namespace |
| } // namespace crubit |