From 40bf69e03c6eb5cd77b79796fa8c6fca80e6ccdf Mon Sep 17 00:00:00 2001 From: Gennaro Prota Date: Mon, 16 Mar 2026 12:22:34 +0100 Subject: [PATCH] refactor: auto-merge symbol fields via Boost.Describe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This replaces hand-written per-field merge logic with a reflection-based approach: mergeReflected() iterates all Boost.Describe'd base classes and members, applying a default type-based strategy via mergeByType(). The strategies are tried in order: bool |=, SymbolID take-if-invalid, ADL merge() dispatch (for types with custom semantics), Polymorphic take-if-placeholder, Polymorphic take-if-identifier-empty, .Implicit types take-if-implicit, Optional take-or-recursive-merge, enum take-if-zero, string take-if-empty, vector dedup-append (when T has operator==), and vector take-if-empty fallback. Every merge function now reduces to mergeReflected(I, Other). Types that need custom merge semantics define their own merge() overload, found via ADL: - merge(ExtractionMode) — leastSpecific() - merge(vector) — element-wise merge + append extras - merge(vector) — dedup by symbol ID - merge(Name) — auto-merge id and Identifier This refactor also fixed some bugs: - Symbol::Parent was merged backwards (took Other when I was valid) - RecordTranche::Usings was silently not merged - FunctionSymbol::IsRecordMethod was never merged - VariableSymbol::IsRecordField was never merged - FunctionSymbol::Attributes was never merged, unlike VariableSymbol::Attributes which was correctly dedup-merged The is_optional and is_vector traits are extracted into a shared ReflectionTypeTraits.hpp to avoid duplication between MapReflectedType.hpp and MergeReflectedType.hpp. --- include/mrdocs/Metadata/Name.hpp | 5 + .../mrdocs/Metadata/Symbol/ExtractionMode.hpp | 11 + include/mrdocs/Metadata/Symbol/Friend.hpp | 12 +- include/mrdocs/Metadata/Symbol/Param.hpp | 10 + src/lib/Metadata/Info.cpp | 27 +- src/lib/Metadata/Name.cpp | 8 + src/lib/Metadata/Symbol/Concept.cpp | 13 +- src/lib/Metadata/Symbol/Enum.cpp | 33 +- src/lib/Metadata/Symbol/EnumConstant.cpp | 9 +- src/lib/Metadata/Symbol/Friend.cpp | 28 +- src/lib/Metadata/Symbol/Function.cpp | 94 +--- src/lib/Metadata/Symbol/Guide.cpp | 21 +- src/lib/Metadata/Symbol/Namespace.cpp | 51 +- src/lib/Metadata/Symbol/NamespaceAlias.cpp | 13 +- src/lib/Metadata/Symbol/Overloads.cpp | 11 +- src/lib/Metadata/Symbol/Record.cpp | 72 +-- src/lib/Metadata/Symbol/Typedef.cpp | 19 +- src/lib/Metadata/Symbol/Using.cpp | 34 +- src/lib/Metadata/Symbol/Variable.cpp | 40 +- .../Support/Reflection/MapReflectedType.hpp | 20 +- .../Support/Reflection/MergeReflectedType.hpp | 281 ++++++++++ src/lib/Support/Reflection/Reflection.cpp | 14 + .../Reflection/ReflectionTypeTraits.hpp | 40 ++ src/test/lib/Metadata/Merge.cpp | 496 ++++++++++++++++++ 24 files changed, 956 insertions(+), 406 deletions(-) create mode 100644 src/lib/Support/Reflection/MergeReflectedType.hpp create mode 100644 src/lib/Support/Reflection/ReflectionTypeTraits.hpp create mode 100644 src/test/lib/Metadata/Merge.cpp diff --git a/include/mrdocs/Metadata/Name.hpp b/include/mrdocs/Metadata/Name.hpp index e816e0cf7b..97a9befee4 100644 --- a/include/mrdocs/Metadata/Name.hpp +++ b/include/mrdocs/Metadata/Name.hpp @@ -89,6 +89,11 @@ tag_invoke( tag_invoke(dom::ValueFromTag{}, v, *I, domCorpus); } +/** Merge two Name metadata objects. +*/ +void +merge(Name& I, Name&& Other); + } // mrdocs #endif diff --git a/include/mrdocs/Metadata/Symbol/ExtractionMode.hpp b/include/mrdocs/Metadata/Symbol/ExtractionMode.hpp index 7b40b3f542..e6845568c7 100644 --- a/include/mrdocs/Metadata/Symbol/ExtractionMode.hpp +++ b/include/mrdocs/Metadata/Symbol/ExtractionMode.hpp @@ -96,6 +96,17 @@ mostSpecific(ExtractionMode const a, ExtractionMode const b) noexcept std::max(static_cast(a), static_cast(b))); } +/** Merge two ExtractionMode values. + + Takes the least specific (most conservative) of the two, + so that a symbol demoted in any TU stays demoted. +*/ +inline void +merge(ExtractionMode& dst, ExtractionMode&& src) noexcept +{ + dst = leastSpecific(dst, src); +} + } // mrdocs #endif // MRDOCS_API_METADATA_SYMBOL_EXTRACTIONMODE_HPP diff --git a/include/mrdocs/Metadata/Symbol/Friend.hpp b/include/mrdocs/Metadata/Symbol/Friend.hpp index ada06d97f4..370a93506a 100644 --- a/include/mrdocs/Metadata/Symbol/Friend.hpp +++ b/include/mrdocs/Metadata/Symbol/Friend.hpp @@ -14,6 +14,7 @@ #include #include #include +#include namespace mrdocs { @@ -42,12 +43,21 @@ struct FriendInfo final Optional> Type = std::nullopt; }; -MRDOCS_DECL /** Merge another FriendInfo into this one. */ +MRDOCS_DECL void merge(FriendInfo& I, FriendInfo&& Other); +/** Merge friend declarations, deduplicating by symbol ID. + + @param dst The destination. + @param src The source (moved from). +*/ +MRDOCS_DECL +void +merge(std::vector& dst, std::vector&& src); + /** Map a FriendInfo to a dom::Object. @param t The tag type. diff --git a/include/mrdocs/Metadata/Symbol/Param.hpp b/include/mrdocs/Metadata/Symbol/Param.hpp index f4c4920d1a..ee9bbf34bb 100644 --- a/include/mrdocs/Metadata/Symbol/Param.hpp +++ b/include/mrdocs/Metadata/Symbol/Param.hpp @@ -18,6 +18,7 @@ #include #include #include +#include #include namespace mrdocs { @@ -68,6 +69,15 @@ MRDOCS_DECL void merge(Param& I, Param&& Other); +/** Merge parameters element-wise, appending extras from `src`. + + @param dst The destination. + @param src The source (moved from). +*/ +MRDOCS_DECL +void +merge(std::vector& dst, std::vector&& src); + /** Return the Param as a @ref dom::Value object. */ MRDOCS_DECL diff --git a/src/lib/Metadata/Info.cpp b/src/lib/Metadata/Info.cpp index 397e770b10..4573971248 100644 --- a/src/lib/Metadata/Info.cpp +++ b/src/lib/Metadata/Info.cpp @@ -11,6 +11,8 @@ // #include +#include +#include #include #include #include @@ -41,30 +43,7 @@ void merge(Symbol& I, Symbol&& Other) { MRDOCS_ASSERT(I.id); - merge(I.Loc, std::move(Other.Loc)); - if (I.Name == "") - { - I.Name = Other.Name; - } - if (I.Parent) - { - I.Parent = Other.Parent; - } - if (I.Access == AccessKind::None) - { - I.Access = Other.Access; - } - I.Extraction = leastSpecific(I.Extraction, Other.Extraction); - - // Append docs - if (!I.doc) - { - I.doc = std::move(Other.doc); - } - else if (Other.doc) - { - merge(*I.doc, std::move(*Other.doc)); - } + mergeReflected(I, Other); } } // mrdocs diff --git a/src/lib/Metadata/Name.cpp b/src/lib/Metadata/Name.cpp index 87deca8af9..4fd349f916 100644 --- a/src/lib/Metadata/Name.cpp +++ b/src/lib/Metadata/Name.cpp @@ -11,6 +11,8 @@ // #include +#include +#include #include #include #include @@ -176,4 +178,10 @@ tag_invoke( v = dom::LazyObject(I, domCorpus); } +void +merge(Name& I, Name&& Other) +{ + mergeReflected(I, Other); +} + } // mrdocs diff --git a/src/lib/Metadata/Symbol/Concept.cpp b/src/lib/Metadata/Symbol/Concept.cpp index 28aea5bf54..dfb27e5ab3 100644 --- a/src/lib/Metadata/Symbol/Concept.cpp +++ b/src/lib/Metadata/Symbol/Concept.cpp @@ -9,9 +9,10 @@ // Official repository: https://github.com/cppalliance/mrdocs // +#include +#include #include #include -#include namespace mrdocs { @@ -62,15 +63,7 @@ void merge(ConceptSymbol& I, ConceptSymbol&& Other) { MRDOCS_ASSERT(canMerge(I, Other)); - merge(I.asInfo(), std::move(Other.asInfo())); - if (I.Constraint.Written.empty()) - { - I.Constraint = std::move(Other.Constraint); - } - if (!I.Template) - { - I.Template = std::move(Other.Template); - } + mergeReflected(I, Other); } } // mrdocs diff --git a/src/lib/Metadata/Symbol/Enum.cpp b/src/lib/Metadata/Symbol/Enum.cpp index d076cf8c71..f93cfd566d 100644 --- a/src/lib/Metadata/Symbol/Enum.cpp +++ b/src/lib/Metadata/Symbol/Enum.cpp @@ -9,45 +9,18 @@ // Official repository: https://github.com/cppalliance/mrdocs // +#include +#include #include #include -#include namespace mrdocs { -namespace { - -void -reduceSymbolIDs( - std::vector& list, - std::vector&& otherList) -{ - for(auto const& id : otherList) - { - if (auto it = llvm::find(list, id); it != list.end()) - { - continue; - } - list.push_back(id); - } -} - -} // (anon) - void merge(EnumSymbol& I, EnumSymbol&& Other) { MRDOCS_ASSERT(canMerge(I, Other)); - merge(I.asInfo(), std::move(Other.asInfo())); - if (!I.Scoped) - { - I.Scoped = Other.Scoped; - } - if (!I.UnderlyingType) - { - I.UnderlyingType = std::move(Other.UnderlyingType); - } - reduceSymbolIDs(I.Constants, std::move(Other.Constants)); + mergeReflected(I, Other); } } // mrdocs diff --git a/src/lib/Metadata/Symbol/EnumConstant.cpp b/src/lib/Metadata/Symbol/EnumConstant.cpp index a8ae6aeaa8..194cddf676 100644 --- a/src/lib/Metadata/Symbol/EnumConstant.cpp +++ b/src/lib/Metadata/Symbol/EnumConstant.cpp @@ -9,9 +9,10 @@ // Official repository: https://github.com/cppalliance/mrdocs // +#include +#include #include #include -#include namespace mrdocs { @@ -19,11 +20,7 @@ void merge(EnumConstantSymbol& I, EnumConstantSymbol&& Other) { MRDOCS_ASSERT(canMerge(I, Other)); - merge(I.asInfo(), std::move(Other.asInfo())); - if (I.Initializer.Written.empty()) - { - I.Initializer = std::move(Other.Initializer); - } + mergeReflected(I, Other); } } // mrdocs diff --git a/src/lib/Metadata/Symbol/Friend.cpp b/src/lib/Metadata/Symbol/Friend.cpp index d4d7f4826a..69f33a8f0e 100644 --- a/src/lib/Metadata/Symbol/Friend.cpp +++ b/src/lib/Metadata/Symbol/Friend.cpp @@ -5,26 +5,40 @@ // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // // Copyright (c) 2025 Alan de Freitas (alandefreitas@gmail.com) +// Copyright (c) 2026 Gennaro Prota (gennaro.prota@gmail.com) // // Official repository: https://github.com/cppalliance/mrdocs // +#include +#include #include #include -#include +#include +#include namespace mrdocs { void merge(FriendInfo& I, FriendInfo&& Other) { - if (!I.id) - { - I.id = Other.id; - } - if (!I.Type) + mergeReflected(I, Other); +} + +void +merge( + std::vector& dst, + std::vector&& src) +{ + for (FriendInfo const& F : src) { - I.Type = std::move(Other.Type); + auto it = std::ranges::find_if(dst, [&F](auto const& other) { + return F.id == other.id; + }); + if (it == dst.end()) + { + dst.push_back(F); + } } } diff --git a/src/lib/Metadata/Symbol/Function.cpp b/src/lib/Metadata/Symbol/Function.cpp index b1eee2da51..e9e80169bd 100644 --- a/src/lib/Metadata/Symbol/Function.cpp +++ b/src/lib/Metadata/Symbol/Function.cpp @@ -11,14 +11,18 @@ // Official repository: https://github.com/cppalliance/mrdocs // +#include +#include #include #include #include #include #include #include +#include #include #include +#include namespace mrdocs { @@ -317,17 +321,23 @@ getOperatorReadableName( void merge(Param& I, Param&& Other) { - if (I.Type->isAuto()) - { - I.Type = std::move(Other.Type); - } - if (!I.Name) + mergeReflected(I, Other); +} + +void +merge(std::vector& dst, std::vector&& src) +{ + std::size_t const n = std::min(dst.size(), src.size()); + for (std::size_t i = 0; i < n; ++i) { - I.Name = std::move(Other.Name); + merge(dst[i], std::move(src[i])); } - if (!I.Default) + if (src.size() > n) { - I.Default = std::move(Other.Default); + dst.insert( + dst.end(), + std::make_move_iterator(src.begin() + n), + std::make_move_iterator(src.end())); } } @@ -411,73 +421,7 @@ void merge(FunctionSymbol& I, FunctionSymbol&& Other) { MRDOCS_ASSERT(canMerge(I, Other)); - merge(I.asInfo(), std::move(Other.asInfo())); - if (I.FuncClass == FunctionClass::Normal) - { - I.FuncClass = Other.FuncClass; - } - I.ReturnType = std::move(Other.ReturnType); - std::size_t const n = std::min(I.Params.size(), Other.Params.size()); - for (std::size_t i = 0; i < n; ++i) - { - merge(I.Params[i], std::move(Other.Params[i])); - } - if (Other.Params.size() > n) - { - I.Params.insert( - I.Params.end(), - std::make_move_iterator(Other.Params.begin() + n), - std::make_move_iterator(Other.Params.end())); - } - if (!I.Template) - { - I.Template = std::move(Other.Template); - } - else if (Other.Template) - { - merge(*I.Template, std::move(*Other.Template)); - } - if (I.Noexcept.Implicit) - { - I.Noexcept = std::move(Other.Noexcept); - } - if (I.Explicit.Implicit) - { - I.Explicit = std::move(Other.Explicit); - } - merge(I.Requires, std::move(Other.Requires)); - I.IsVariadic |= Other.IsVariadic; - I.IsVirtual |= Other.IsVirtual; - I.IsVirtualAsWritten |= Other.IsVirtualAsWritten; - I.IsPure |= Other.IsPure; - I.IsDefaulted |= Other.IsDefaulted; - I.IsExplicitlyDefaulted |= Other.IsExplicitlyDefaulted; - I.IsDeleted |= Other.IsDeleted; - I.IsDeletedAsWritten |= Other.IsDeletedAsWritten; - I.IsNoReturn |= Other.IsNoReturn; - I.HasOverrideAttr |= Other.HasOverrideAttr; - I.HasTrailingReturn |= Other.HasTrailingReturn; - I.IsConst |= Other.IsConst; - I.IsVolatile |= Other.IsVolatile; - I.IsFinal |= Other.IsFinal; - I.IsNodiscard |= Other.IsNodiscard; - I.IsExplicitObjectMemberFunction |= Other.IsExplicitObjectMemberFunction; - if (I.Constexpr == ConstexprKind::None) - { - I.Constexpr = Other.Constexpr; - } - if (I.StorageClass == StorageClassKind::None) - { - I.StorageClass = Other.StorageClass; - } - if (I.RefQualifier == ReferenceKind::None) - { - I.RefQualifier = Other.RefQualifier; - } - if (I.OverloadedOperator == OperatorKind::None) - { - I.OverloadedOperator = Other.OverloadedOperator; - } + mergeReflected(I, Other); } bool diff --git a/src/lib/Metadata/Symbol/Guide.cpp b/src/lib/Metadata/Symbol/Guide.cpp index a9a1e41227..02efba1b15 100644 --- a/src/lib/Metadata/Symbol/Guide.cpp +++ b/src/lib/Metadata/Symbol/Guide.cpp @@ -9,10 +9,11 @@ // Official repository: https://github.com/cppalliance/mrdocs // +#include +#include #include #include #include -#include #include namespace mrdocs { @@ -82,23 +83,7 @@ operator<=>(GuideSymbol const& other) const void merge(GuideSymbol& I, GuideSymbol&& Other) { MRDOCS_ASSERT(canMerge(I, Other)); - merge(I.asInfo(), std::move(Other.asInfo())); - if (I.Deduced->isAuto()) - { - I.Deduced = std::move(Other.Deduced); - } - if (I.Params.empty()) - { - I.Params = std::move(Other.Params); - } - if (!I.Template) - { - I.Template = std::move(Other.Template); - } - if (I.Explicit.Implicit) - { - I.Explicit = std::move(Other.Explicit); - } + mergeReflected(I, Other); } } // mrdocs diff --git a/src/lib/Metadata/Symbol/Namespace.cpp b/src/lib/Metadata/Symbol/Namespace.cpp index b38fc9073a..e3438961a9 100644 --- a/src/lib/Metadata/Symbol/Namespace.cpp +++ b/src/lib/Metadata/Symbol/Namespace.cpp @@ -9,6 +9,8 @@ // Official repository: https://github.com/cppalliance/mrdocs // +#include +#include #include #include #include @@ -55,62 +57,17 @@ operator<=>(NamespaceSymbol const& other) const return std::strong_ordering::equal; } -namespace { -void -reduceSymbolIDs( - std::vector& list, - std::vector&& otherList) -{ - for(auto const& id : otherList) - { - if (auto it = llvm::find(list, id); it != list.end()) - { - continue; - } - list.push_back(id); - } -} - -void -reduceNames( - std::vector& list, - std::vector&& otherList) -{ - for(auto const& id : otherList) - { - if (auto it = llvm::find(list, id); it != list.end()) - { - continue; - } - list.push_back(id); - } -} -} // (anon) - void merge(NamespaceTranche& I, NamespaceTranche&& Other) { - reduceSymbolIDs(I.Namespaces, std::move(Other.Namespaces)); - reduceSymbolIDs(I.NamespaceAliases, std::move(Other.NamespaceAliases)); - reduceSymbolIDs(I.Typedefs, std::move(Other.Typedefs)); - reduceSymbolIDs(I.Records, std::move(Other.Records)); - reduceSymbolIDs(I.Enums, std::move(Other.Enums)); - reduceSymbolIDs(I.Functions, std::move(Other.Functions)); - reduceSymbolIDs(I.Variables, std::move(Other.Variables)); - reduceSymbolIDs(I.Concepts, std::move(Other.Concepts)); - reduceSymbolIDs(I.Guides, std::move(Other.Guides)); - reduceSymbolIDs(I.Usings, std::move(Other.Usings)); + mergeReflected(I, Other); } void merge(NamespaceSymbol& I, NamespaceSymbol&& Other) { MRDOCS_ASSERT(canMerge(I, Other)); - merge(I.asInfo(), std::move(Other.asInfo())); - merge(I.Members, std::move(Other.Members)); - reduceNames(I.UsingDirectives, std::move(Other.UsingDirectives)); - I.IsInline |= Other.IsInline; - I.IsAnonymous |= Other.IsAnonymous; + mergeReflected(I, Other); } } // mrdocs diff --git a/src/lib/Metadata/Symbol/NamespaceAlias.cpp b/src/lib/Metadata/Symbol/NamespaceAlias.cpp index 379c6cf94b..b7f8b7bfe3 100644 --- a/src/lib/Metadata/Symbol/NamespaceAlias.cpp +++ b/src/lib/Metadata/Symbol/NamespaceAlias.cpp @@ -9,9 +9,10 @@ // Official repository: https://github.com/cppalliance/mrdocs // +#include +#include #include #include -#include namespace mrdocs { @@ -19,15 +20,7 @@ void merge(NamespaceAliasSymbol& I, NamespaceAliasSymbol&& Other) { MRDOCS_ASSERT(canMerge(I, Other)); - merge(I.asInfo(), std::move(Other.asInfo())); - if (I.AliasedSymbol.Identifier.empty()) - { - I.AliasedSymbol.Identifier = std::move(Other.AliasedSymbol.Identifier); - } - if (!I.AliasedSymbol.id) - { - I.AliasedSymbol.id = Other.AliasedSymbol.id; - } + mergeReflected(I, Other); } } // mrdocs diff --git a/src/lib/Metadata/Symbol/Overloads.cpp b/src/lib/Metadata/Symbol/Overloads.cpp index 0d505b7318..714af0d08d 100644 --- a/src/lib/Metadata/Symbol/Overloads.cpp +++ b/src/lib/Metadata/Symbol/Overloads.cpp @@ -9,13 +9,14 @@ // #include +#include +#include #include #include #include #include #include #include -#include #include #include @@ -31,13 +32,7 @@ OverloadsSymbol::OverloadsSymbol(SymbolID const &Parent, std::string_view Name, void merge(OverloadsSymbol& I, OverloadsSymbol&& Other) { - merge(I.asInfo(), std::move(Other.asInfo())); - namespace stdr = std::ranges; - namespace stdv = std::ranges::views; - auto newMembers = stdv::filter(Other.Members, [&](auto const& Member) { - return stdr::find(I.Members, Member) == I.Members.end(); - }); - I.Members.insert(I.Members.end(), newMembers.begin(), newMembers.end()); + mergeReflected(I, Other); } void diff --git a/src/lib/Metadata/Symbol/Record.cpp b/src/lib/Metadata/Symbol/Record.cpp index 4aaf3f12a8..854db40b9a 100644 --- a/src/lib/Metadata/Symbol/Record.cpp +++ b/src/lib/Metadata/Symbol/Record.cpp @@ -9,50 +9,15 @@ // Official repository: https://github.com/cppalliance/mrdocs // +#include #include #include #include #include #include -#include namespace mrdocs { -namespace { -void -reduceSymbolIDs( - std::vector& list, - std::vector&& otherList) -{ - for(auto const& id : otherList) - { - if (auto it = std::ranges::find(list, id); it != list.end()) - { - continue; - } - list.push_back(id); - } -} - -void -reduceSymbolIDs( - std::vector& list, - std::vector&& otherList) -{ - for(auto const& F : otherList) - { - auto it = std::ranges::find_if(list, [&F](auto const& other) { - return F.id == other.id; - }); - if (it != list.end()) - { - continue; - } - list.push_back(F); - } -} -} // (anon) - std::strong_ordering RecordSymbol:: operator<=>(RecordSymbol const& other) const @@ -99,48 +64,19 @@ operator<=>(RecordSymbol const& other) const void merge(RecordTranche& I, RecordTranche&& Other) { - reduceSymbolIDs(I.NamespaceAliases, std::move(Other.NamespaceAliases)); - reduceSymbolIDs(I.Typedefs, std::move(Other.Typedefs)); - reduceSymbolIDs(I.Records, std::move(Other.Records)); - reduceSymbolIDs(I.Enums, std::move(Other.Enums)); - reduceSymbolIDs(I.Functions, std::move(Other.Functions)); - reduceSymbolIDs(I.StaticFunctions, std::move(Other.StaticFunctions)); - reduceSymbolIDs(I.Variables, std::move(Other.Variables)); - reduceSymbolIDs(I.StaticVariables, std::move(Other.StaticVariables)); - reduceSymbolIDs(I.Concepts, std::move(Other.Concepts)); - reduceSymbolIDs(I.Guides, std::move(Other.Guides)); + mergeReflected(I, Other); } void merge(RecordInterface& I, RecordInterface&& Other) { - merge(I.Public, std::move(Other.Public)); - merge(I.Protected, std::move(Other.Protected)); - merge(I.Private, std::move(Other.Private)); + mergeReflected(I, Other); } void merge(RecordSymbol& I, RecordSymbol&& Other) { - merge(I.asInfo(), std::move(Other.asInfo())); - if (Other.KeyKind != RecordKeyKind::Struct && - I.KeyKind != Other.KeyKind) - { - I.KeyKind = Other.KeyKind; - } - I.IsTypeDef |= Other.IsTypeDef; - I.IsFinal |= Other.IsFinal; - I.IsFinalDestructor |= Other.IsFinalDestructor; - if (I.Bases.empty()) - { - I.Bases = std::move(Other.Bases); - } - merge(I.Interface, std::move(Other.Interface)); - if (!I.Template) - { - I.Template = std::move(Other.Template); - } - reduceSymbolIDs(I.Friends, std::move(Other.Friends)); + mergeReflected(I, Other); } template diff --git a/src/lib/Metadata/Symbol/Typedef.cpp b/src/lib/Metadata/Symbol/Typedef.cpp index e6d798e8fd..35e32b1f0e 100644 --- a/src/lib/Metadata/Symbol/Typedef.cpp +++ b/src/lib/Metadata/Symbol/Typedef.cpp @@ -9,10 +9,11 @@ // Official repository: https://github.com/cppalliance/mrdocs // +#include +#include #include #include #include -#include namespace mrdocs { @@ -62,21 +63,7 @@ operator<=>(TypedefSymbol const& other) const void merge(TypedefSymbol& I, TypedefSymbol&& Other) { MRDOCS_ASSERT(canMerge(I, Other)); - merge(I.asInfo(), std::move(Other.asInfo())); - if (!I.IsUsing) - { - I.IsUsing = Other.IsUsing; - } - MRDOCS_ASSERT(!I.Type.valueless_after_move()); - if (I.Type->isNamed() && - I.Type->asNamed().Name->Identifier.empty()) - { - I.Type = std::move(Other.Type); - } - if (!I.Template) - { - I.Template = std::move(Other.Template); - } + mergeReflected(I, Other); } } // mrdocs diff --git a/src/lib/Metadata/Symbol/Using.cpp b/src/lib/Metadata/Symbol/Using.cpp index a37da7ac87..d39390aa5c 100644 --- a/src/lib/Metadata/Symbol/Using.cpp +++ b/src/lib/Metadata/Symbol/Using.cpp @@ -10,46 +10,18 @@ // #include +#include +#include #include #include -#include namespace mrdocs { -namespace { - -void -reduceSymbolIDs( - std::vector& list, - std::vector&& otherList) -{ - for(auto const& id : otherList) - { - if (auto it = llvm::find(list, id); it != list.end()) - { - continue; - } - list.push_back(id); - } -} - -} // (anon) - void merge(UsingSymbol& I, UsingSymbol&& Other) { MRDOCS_ASSERT(canMerge(I, Other)); - merge(I.asInfo(), std::move(Other.asInfo())); - reduceSymbolIDs(I.ShadowDeclarations, std::move(Other.ShadowDeclarations)); - if (I.Class == UsingClass::Normal) - { - I.Class = Other.Class; - } - MRDOCS_ASSERT(!I.IntroducedName.valueless_after_move()); - if (I.IntroducedName->Identifier.empty()) - { - I.IntroducedName = std::move(Other.IntroducedName); - } + mergeReflected(I, Other); } void diff --git a/src/lib/Metadata/Symbol/Variable.cpp b/src/lib/Metadata/Symbol/Variable.cpp index 8251ff1e90..97550bd299 100644 --- a/src/lib/Metadata/Symbol/Variable.cpp +++ b/src/lib/Metadata/Symbol/Variable.cpp @@ -9,6 +9,8 @@ // Official repository: https://github.com/cppalliance/mrdocs // +#include +#include #include #include #include @@ -62,43 +64,7 @@ void merge(VariableSymbol& I, VariableSymbol&& Other) { MRDOCS_ASSERT(canMerge(I, Other)); - merge(I.asInfo(), std::move(Other.asInfo())); - MRDOCS_ASSERT(!I.Type.valueless_after_move()); - if (I.Type->isNamed() && - I.Type->asNamed().Name->Identifier.empty()) - { - I.Type = std::move(Other.Type); - } - if (!I.Template) - { - I.Template = std::move(Other.Template); - } - if (I.Initializer.Written.empty()) - { - I.Initializer = std::move(Other.Initializer); - } - I.IsConstinit |= Other.IsConstinit; - I.IsThreadLocal |= Other.IsThreadLocal; - I.IsConstexpr |= Other.IsConstexpr; - I.IsInline |= Other.IsInline; - if (I.StorageClass == StorageClassKind::None) - { - I.StorageClass = Other.StorageClass; - } - for (auto& otherAttr: Other.Attributes) - { - if (std::ranges::find(I.Attributes, otherAttr) == I.Attributes.end()) - { - I.Attributes.push_back(otherAttr); - } - } - I.IsBitfield |= Other.IsBitfield; - merge(I.BitfieldWidth, std::move(Other.BitfieldWidth)); - I.IsVariant |= Other.IsVariant; - I.IsMutable |= Other.IsMutable; - I.IsMaybeUnused |= Other.IsMaybeUnused; - I.IsDeprecated |= Other.IsDeprecated; - I.HasNoUniqueAddress |= Other.HasNoUniqueAddress; + mergeReflected(I, Other); } } // mrdocs diff --git a/src/lib/Support/Reflection/MapReflectedType.hpp b/src/lib/Support/Reflection/MapReflectedType.hpp index 4071150289..bdac58e571 100644 --- a/src/lib/Support/Reflection/MapReflectedType.hpp +++ b/src/lib/Support/Reflection/MapReflectedType.hpp @@ -12,6 +12,7 @@ #define MRDOCS_LIB_SUPPORT_REFLECTION_MAPREFLECTEDTYPE_HPP #include "ReadableTypeName.hpp" +#include "ReflectionTypeTraits.hpp" #include #include #include @@ -31,23 +32,6 @@ class DomCorpus; namespace detail { -/** Type traits to identify special types that need custom handling. -*/ -template -struct is_vector : std::false_type {}; - -template -struct is_vector> : std::true_type {}; - -template -inline constexpr bool is_vector_v = is_vector::value; - -template struct is_optional : std::false_type {}; -template struct is_optional> : std::true_type {}; - -template -inline constexpr bool is_optional_v = is_optional::value; - /** Helper to determine if a member should be mapped based on its value. */ template @@ -147,7 +131,7 @@ mapMember( if constexpr (detail::is_vector_v) { - // Vectors become lazy arrays — the decision is encapsulated here. + // Vectors become lazy arrays — the decision is encapsulated here. MRDOCS_ASSERT(domCorpus != nullptr); io.map(domName, dom::LazyArray(value, domCorpus)); } diff --git a/src/lib/Support/Reflection/MergeReflectedType.hpp b/src/lib/Support/Reflection/MergeReflectedType.hpp new file mode 100644 index 0000000000..edcba7122b --- /dev/null +++ b/src/lib/Support/Reflection/MergeReflectedType.hpp @@ -0,0 +1,281 @@ +// +// Licensed under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Copyright (c) 2026 Gennaro Prota (gennaro.prota@gmail.com) +// +// Official repository: https://github.com/cppalliance/mrdocs +// + +#ifndef MRDOCS_LIB_SUPPORT_REFLECTION_MERGEREFLECTEDTYPE_HPP +#define MRDOCS_LIB_SUPPORT_REFLECTION_MERGEREFLECTEDTYPE_HPP + +#include "ReflectionTypeTraits.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace mrdocs { + +// Forward declarations — full definitions are provided by the +// translation units that instantiate mergeReflected. +class SymbolID; +struct Type; +struct Name; + +namespace detail { + +/** Determine if an enum value is at its default (i.e., zero-initialized). + + Most enums in MrDocs use 0 as their "unset" value (e.g., `None`, + `Normal`, `Struct`). This template detects that uniformly. +*/ +template + requires std::is_enum_v +constexpr bool +isDefaultEnum(E value) +{ + return static_cast>(value) == 0; +} + +// Type trait: is this Polymorphic for a given U? +template +inline constexpr bool is_polymorphic_v = false; + +template +inline constexpr bool is_polymorphic_v, U> = true; + +// Type trait: can we call merge(T&, T&&) via ADL? +template +struct has_adl_merge : std::false_type {}; + +template +struct has_adl_merge(), std::declval()))>> + : std::true_type {}; + +template +inline constexpr bool has_adl_merge_v = has_adl_merge::value; + +// Type trait: is this a vector whose element type has operator==? +template +inline constexpr bool is_equality_comparable_vector_v = false; + +template + requires std::equality_comparable +inline constexpr bool is_equality_comparable_vector_v> = true; + +/** Check if a Polymorphic\ is in a placeholder state. + + A type is a placeholder when it holds AutoType{} (the default + for function return types and parameters) or a blank NamedType + with an empty Identifier (the default for variable and typedef + types). Both represent "unknown type, to be filled in." + + Defined in Reflection.cpp to avoid including heavy Type headers. +*/ +MRDOCS_DECL +bool +isPlaceholderType(Polymorphic const& t); + +/** Merge a single member using a default type-based strategy. + + The strategies are tried in this order: + + 1. `bool` — `dst = dst | src` + 2. `SymbolID` — take if invalid + 3. ADL `merge()` — custom merge function (ExtractionMode, + ExprInfo, SourceInfo, vector\, etc.) + 4. `Polymorphic` — take if placeholder (AutoType or blank + NamedType) + 5. `Polymorphic` — take if Identifier is empty + 6. `.Implicit` types — take if dst is implicit + 7. `Optional` — take if disengaged; recursive merge + if both engaged and T has `merge()` + 8. `enum` — take if zero-initialized + 9. `string` — take if empty + 10. `vector` with `==` — dedup-append + 11. `vector` fallback — take if dst is empty + + Returns `false` only for types none of the above handles. +*/ +template +bool +mergeByType(T& dst, T&& src) +{ + // bool: OR-merge (any TU seeing `true` wins). + if constexpr (std::is_same_v) + { + dst = dst | src; + return true; + } + // SymbolID: take src if dst is invalid. + else if constexpr (std::is_same_v) + { + if (!dst) + { + dst = src; + } + return true; + } + // ADL merge: custom merge function takes priority over + // generic strategies. Catches ExtractionMode, ExprInfo, + // SourceInfo, vector, vector, etc. + else if constexpr (has_adl_merge_v) + { + merge(dst, std::move(src)); + return true; + } + // Polymorphic: take src if dst is in a placeholder + // state — either AutoType{} or a blank NamedType with an + // empty Identifier. + else if constexpr (is_polymorphic_v) + { + if (isPlaceholderType(dst)) + { + dst = std::move(src); + } + return true; + } + // Polymorphic: take src if dst has an empty Identifier. + else if constexpr (is_polymorphic_v) + { + if (dst->Identifier.empty()) + { + dst = std::move(src); + } + return true; + } + // Types with .Implicit flag (NoexceptInfo, ExplicitInfo): + // take src if dst is still implicit (compiler-generated). + else if constexpr (requires(T const& t) { { t.Implicit } -> std::convertible_to; }) + { + if (dst.Implicit) + { + dst = std::move(src); + } + return true; + } + // Optional: take if disengaged; recursive merge if both + // engaged and the value type has a merge() function. + else if constexpr (is_optional_v) + { + if (!dst) + { + dst = std::move(src); + } + else if constexpr (has_adl_merge_v) + { + if (src) + { + merge(*dst, std::move(*src)); + } + } + return true; + } + // enum: take src if dst is zero-initialized. + else if constexpr (std::is_enum_v) + { + if (isDefaultEnum(dst)) + { + dst = src; + } + return true; + } + // string: take src if dst is empty. + else if constexpr (std::is_same_v) + { + if (dst.empty()) + { + dst = std::move(src); + } + return true; + } + // vector where T has operator==: dedup-append. + else if constexpr (is_equality_comparable_vector_v) + { + for (auto& elem : src) + { + if (std::ranges::find(dst, elem) == dst.end()) + { + dst.push_back(std::move(elem)); + } + } + return true; + } + // vector fallback: take if dst is empty. + else if constexpr (is_vector_v) + { + if (dst.empty()) + { + dst = std::move(src); + } + return true; + } + else + { + return false; + } +} + +} // namespace detail + +/** Merge all Boost.Describe'd members of a type. + + Iterates base classes (via `describe_bases`) and own members + (via `describe_members`). + + Base classes are merged first by calling `merge(base_dst, + base_src)`, which must be found via ADL. + + For each own member, a default merge strategy is applied + based on the member type (see `mergeByType` for the full + list of strategies). + + @tparam T The type to merge (must have BOOST_DESCRIBE_STRUCT). + @param dst The destination object. + @param src The source object. Members are moved from + individually. +*/ +template + requires boost::describe::has_describe_members::value +void +mergeReflected( + T& dst, + T& src) +{ + // First, merge all base classes. + boost::mp11::mp_for_each< + boost::describe::describe_bases>( + [&](auto const& descriptor) + { + using BaseType = typename std::decay_t::type; + merge( + static_cast(dst), + static_cast(src)); + } + ); + + // Then, merge all own members. + boost::mp11::mp_for_each< + boost::describe::describe_members>( + [&](auto const& descriptor) + { + using Descriptor = std::decay_t; + + auto& dstMember = dst.*Descriptor::pointer; + auto&& srcMember = std::move(src.*Descriptor::pointer); + + detail::mergeByType(dstMember, std::move(srcMember)); + }); +} + +} // namespace mrdocs + +#endif diff --git a/src/lib/Support/Reflection/Reflection.cpp b/src/lib/Support/Reflection/Reflection.cpp index de0ea7e16e..ced5a9fb20 100644 --- a/src/lib/Support/Reflection/Reflection.cpp +++ b/src/lib/Support/Reflection/Reflection.cpp @@ -8,11 +8,25 @@ // Official repository: https://github.com/cppalliance/mrdocs // +#include "MergeReflectedType.hpp" #include "Reflection.hpp" #include "MapReflectedType.hpp" +#include namespace mrdocs { +namespace detail { + +bool +isPlaceholderType(Polymorphic const& t) +{ + return t->isAuto() || + (t->isNamed() && + t->asNamed().Name->Identifier.empty()); +} + +} // namespace detail + //------------------------------------------------------ // Symbol. //------------------------------------------------------ diff --git a/src/lib/Support/Reflection/ReflectionTypeTraits.hpp b/src/lib/Support/Reflection/ReflectionTypeTraits.hpp new file mode 100644 index 0000000000..b6f35d4b00 --- /dev/null +++ b/src/lib/Support/Reflection/ReflectionTypeTraits.hpp @@ -0,0 +1,40 @@ +// +// Licensed under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Copyright (c) 2025 Gennaro Prota (gennaro.prota@gmail.com) +// +// Official repository: https://github.com/cppalliance/mrdocs +// + +#ifndef MRDOCS_LIB_SUPPORT_REFLECTION_REFLECTIONTYPETRAITS_HPP +#define MRDOCS_LIB_SUPPORT_REFLECTION_REFLECTIONTYPETRAITS_HPP + +#include +#include +#include + +namespace mrdocs::detail { + +template +struct is_optional : std::false_type {}; + +template +struct is_optional> : std::true_type {}; + +template +inline constexpr bool is_optional_v = is_optional::value; + +template +struct is_vector : std::false_type {}; + +template +struct is_vector> : std::true_type {}; + +template +inline constexpr bool is_vector_v = is_vector::value; + +} + +#endif diff --git a/src/test/lib/Metadata/Merge.cpp b/src/test/lib/Metadata/Merge.cpp new file mode 100644 index 0000000000..8c432c4a44 --- /dev/null +++ b/src/test/lib/Metadata/Merge.cpp @@ -0,0 +1,496 @@ +// +// Licensed under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// Copyright (c) 2026 Gennaro Prota (gennaro.prota@gmail.com) +// +// Official repository: https://github.com/cppalliance/mrdocs +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mrdocs { +namespace { + +// ------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------ + +SymbolID const id1("12345678901234567890"); +SymbolID const id2("abcdefghijklmnopqrst"); + +Polymorphic +makeNamedType(std::string identifier) +{ + NamedType nt; + nt.Name->Identifier = std::move(identifier); + return Polymorphic(std::move(nt)); +} + +FunctionSymbol +makeFunc() +{ + FunctionSymbol f(id1); + f.Name = "f"; + return f; +} + +EnumSymbol +makeEnum() +{ + EnumSymbol e(id1); + e.Name = "E"; + return e; +} + +// ------------------------------------------------------------------ + +struct MergeTest +{ + // -- Param ---------------------------------------------------- + + void test_param_type_placeholder_takes_src() + { + Param dst; // Type = AutoType{} (placeholder) + Param src; + src.Type = makeNamedType("int"); + + merge(dst, std::move(src)); + BOOST_TEST(dst.Type->isNamed()); + } + + void test_param_type_non_placeholder_keeps_dst() + { + Param dst; + dst.Type = makeNamedType("int"); + Param src; + src.Type = makeNamedType("double"); + + merge(dst, std::move(src)); + BOOST_TEST(dst.Type->asNamed().Name->Identifier == "int"); + } + + void test_param_name_empty_takes_src() + { + Param dst; + Param src; + src.Name = "x"; + + merge(dst, std::move(src)); + BOOST_TEST(dst.Name.has_value()); + BOOST_TEST(*dst.Name == "x"); + } + + void test_param_name_set_keeps_dst() + { + Param dst; + dst.Name = "a"; + Param src; + src.Name = "b"; + + merge(dst, std::move(src)); + BOOST_TEST(*dst.Name == "a"); + } + + void test_param_default_empty_takes_src() + { + Param dst; + Param src; + src.Default = "42"; + + merge(dst, std::move(src)); + BOOST_TEST(dst.Default.has_value()); + BOOST_TEST(*dst.Default == "42"); + } + + // -- vector -------------------------------------------- + + void test_param_vector_element_wise() + { + Param p1; + p1.Name = "x"; + std::vector dst = { std::move(p1) }; + + Param s1; + s1.Default = "0"; + std::vector src = { std::move(s1) }; + + merge(dst, std::move(src)); + BOOST_TEST(dst.size() == 1u); + BOOST_TEST(*dst[0].Name == "x"); + BOOST_TEST(*dst[0].Default == "0"); + } + + void test_param_vector_src_has_extras() + { + std::vector dst; + Param s1; + s1.Name = "a"; + Param s2; + s2.Name = "b"; + std::vector src = { std::move(s1), std::move(s2) }; + + merge(dst, std::move(src)); + BOOST_TEST(dst.size() == 2u); + BOOST_TEST(*dst[0].Name == "a"); + BOOST_TEST(*dst[1].Name == "b"); + } + + void test_param_vector_dst_has_extras() + { + Param p1; + p1.Name = "x"; + Param p2; + p2.Name = "y"; + std::vector dst = { std::move(p1), std::move(p2) }; + std::vector src; + + merge(dst, std::move(src)); + BOOST_TEST(dst.size() == 2u); + } + + // -- FriendInfo ----------------------------------------------- + + void test_friend_id_invalid_takes_src() + { + FriendInfo dst; + FriendInfo src; + src.id = id1; + + merge(dst, std::move(src)); + BOOST_TEST(dst.id == id1); + } + + void test_friend_id_valid_keeps_dst() + { + FriendInfo dst; + dst.id = id1; + FriendInfo src; + src.id = id2; + + merge(dst, std::move(src)); + BOOST_TEST(dst.id == id1); + } + + void test_friend_type_empty_takes_src() + { + FriendInfo dst; + FriendInfo src; + src.Type = Polymorphic(NamedType{}); + + merge(dst, std::move(src)); + BOOST_TEST(dst.Type.has_value()); + } + + // -- vector --------------------------------------- + + void test_friend_vector_dedup_by_id() + { + FriendInfo f1; + f1.id = id1; + std::vector dst = { f1 }; + + FriendInfo s1; + s1.id = id1; // duplicate + FriendInfo s2; + s2.id = id2; // new + std::vector src = { std::move(s1), std::move(s2) }; + + merge(dst, std::move(src)); + BOOST_TEST(dst.size() == 2u); + } + + void test_friend_vector_empty_dst() + { + std::vector dst; + FriendInfo s1; + s1.id = id1; + std::vector src = { std::move(s1) }; + + merge(dst, std::move(src)); + BOOST_TEST(dst.size() == 1u); + BOOST_TEST(dst[0].id == id1); + } + + // -- Symbol (base class fields) ------------------------------- + + void test_symbol_name_empty_takes_src() + { + auto dst = makeFunc(); + dst.Name = ""; + auto src = makeFunc(); + src.Name = "foo"; + + merge(static_cast(dst), static_cast(src)); + BOOST_TEST(dst.Name == "foo"); + } + + void test_symbol_name_set_keeps_dst() + { + auto dst = makeFunc(); + dst.Name = "bar"; + auto src = makeFunc(); + src.Name = "baz"; + + merge(static_cast(dst), static_cast(src)); + BOOST_TEST(dst.Name == "bar"); + } + + void test_symbol_parent_invalid_takes_src() + { + auto dst = makeFunc(); + auto src = makeFunc(); + src.Parent = id2; + + merge(static_cast(dst), static_cast(src)); + BOOST_TEST(dst.Parent == id2); + } + + void test_symbol_parent_valid_keeps_dst() + { + auto dst = makeFunc(); + dst.Parent = id1; + auto src = makeFunc(); + src.Parent = id2; + + merge(static_cast(dst), static_cast(src)); + BOOST_TEST(dst.Parent == id1); + } + + void test_symbol_access_none_takes_src() + { + auto dst = makeFunc(); + auto src = makeFunc(); + src.Access = AccessKind::Public; + + merge(static_cast(dst), static_cast(src)); + BOOST_TEST(dst.Access == AccessKind::Public); + } + + void test_symbol_bool_or_merge() + { + auto dst = makeFunc(); + auto src = makeFunc(); + src.IsCopyFromInherited = true; + + merge(static_cast(dst), static_cast(src)); + BOOST_TEST(dst.IsCopyFromInherited); + } + + // -- FunctionSymbol ------------------------------------------- + + void test_func_noexcept_implicit_takes_src() + { + auto dst = makeFunc(); + auto src = makeFunc(); + src.Noexcept.Implicit = false; + src.Noexcept.Kind = NoexceptKind::True; + + merge(dst, std::move(src)); + BOOST_TEST(!dst.Noexcept.Implicit); + BOOST_TEST(dst.Noexcept.Kind == NoexceptKind::True); + } + + void test_func_noexcept_explicit_keeps_dst() + { + auto dst = makeFunc(); + dst.Noexcept.Implicit = false; + dst.Noexcept.Kind = NoexceptKind::True; + auto src = makeFunc(); + src.Noexcept.Implicit = false; + src.Noexcept.Kind = NoexceptKind::False; + + merge(dst, std::move(src)); + BOOST_TEST(dst.Noexcept.Kind == NoexceptKind::True); + } + + void test_func_explicit_implicit_takes_src() + { + auto dst = makeFunc(); + auto src = makeFunc(); + src.Explicit.Implicit = false; + src.Explicit.Kind = ExplicitKind::True; + + merge(dst, std::move(src)); + BOOST_TEST(!dst.Explicit.Implicit); + } + + void test_func_attributes_dedup() + { + auto dst = makeFunc(); + dst.Attributes.push_back("nodiscard"); + auto src = makeFunc(); + src.Attributes.push_back("nodiscard"); + src.Attributes.push_back("deprecated"); + + merge(dst, std::move(src)); + BOOST_TEST(dst.Attributes.size() == 2u); + } + + void test_func_bool_fields_or() + { + auto dst = makeFunc(); + auto src = makeFunc(); + src.IsRecordMethod = true; + src.IsVariadic = true; + + merge(dst, std::move(src)); + BOOST_TEST(dst.IsRecordMethod); + BOOST_TEST(dst.IsVariadic); + } + + void test_func_enum_takes_nonzero() + { + auto dst = makeFunc(); + auto src = makeFunc(); + src.FuncClass = FunctionClass::Constructor; + + merge(dst, std::move(src)); + BOOST_TEST(dst.FuncClass == FunctionClass::Constructor); + } + + // -- EnumSymbol ----------------------------------------------- + + void test_enum_constants_dedup() + { + auto dst = makeEnum(); + dst.Constants.push_back(id1); + + auto src = makeEnum(); + src.Constants.push_back(id1); // duplicate + src.Constants.push_back(id2); // new + + merge(dst, std::move(src)); + BOOST_TEST(dst.Constants.size() == 2u); + } + + void test_enum_scoped_bool_or() + { + auto dst = makeEnum(); + auto src = makeEnum(); + src.Scoped = true; + + merge(dst, std::move(src)); + BOOST_TEST(dst.Scoped); + } + + // -- RecordTranche -------------------------------------------- + + void test_record_tranche_dedup_append() + { + RecordTranche dst; + dst.Records.push_back(id1); + + RecordTranche src; + src.Records.push_back(id1); // duplicate + src.Records.push_back(id2); // new + src.Functions.push_back(id1); + + merge(dst, std::move(src)); + BOOST_TEST(dst.Records.size() == 2u); + BOOST_TEST(dst.Functions.size() == 1u); + } + + // -- NamespaceTranche ----------------------------------------- + + void test_namespace_tranche_dedup_append() + { + NamespaceTranche dst; + dst.Namespaces.push_back(id1); + dst.Usings.push_back(id2); + + NamespaceTranche src; + src.Namespaces.push_back(id1); // duplicate + src.Namespaces.push_back(id2); // new + src.Usings.push_back(id2); // duplicate + + merge(dst, std::move(src)); + BOOST_TEST(dst.Namespaces.size() == 2u); + BOOST_TEST(dst.Usings.size() == 1u); + } + + // -- Name ----------------------------------------------------- + + void test_name_identifier_empty_takes_src() + { + IdentifierName dst; + IdentifierName src; + src.Identifier = "ns"; + src.id = id1; + + merge(static_cast(dst), static_cast(src)); + BOOST_TEST(dst.Identifier == "ns"); + BOOST_TEST(dst.id == id1); + } + + void test_name_identifier_set_keeps_dst() + { + IdentifierName dst; + dst.Identifier = "original"; + IdentifierName src; + src.Identifier = "other"; + + merge(static_cast(dst), static_cast(src)); + BOOST_TEST(dst.Identifier == "original"); + } + + // -- run ------------------------------------------------------ + + void run() + { + test_param_type_placeholder_takes_src(); + test_param_type_non_placeholder_keeps_dst(); + test_param_name_empty_takes_src(); + test_param_name_set_keeps_dst(); + test_param_default_empty_takes_src(); + + test_param_vector_element_wise(); + test_param_vector_src_has_extras(); + test_param_vector_dst_has_extras(); + + test_friend_id_invalid_takes_src(); + test_friend_id_valid_keeps_dst(); + test_friend_type_empty_takes_src(); + + test_friend_vector_dedup_by_id(); + test_friend_vector_empty_dst(); + + test_symbol_name_empty_takes_src(); + test_symbol_name_set_keeps_dst(); + test_symbol_parent_invalid_takes_src(); + test_symbol_parent_valid_keeps_dst(); + test_symbol_access_none_takes_src(); + test_symbol_bool_or_merge(); + + test_func_noexcept_implicit_takes_src(); + test_func_noexcept_explicit_keeps_dst(); + test_func_explicit_implicit_takes_src(); + test_func_attributes_dedup(); + test_func_bool_fields_or(); + test_func_enum_takes_nonzero(); + + test_enum_constants_dedup(); + test_enum_scoped_bool_or(); + + test_record_tranche_dedup_append(); + test_namespace_tranche_dedup_append(); + + test_name_identifier_empty_takes_src(); + test_name_identifier_set_keeps_dst(); + } +}; + +} // (anon) + +TEST_SUITE(MergeTest, "clang.mrdocs.Metadata.Merge"); + +} // mrdocs