From 6ebdc8ab77de24e97a9905e244116cb319b5ca73 Mon Sep 17 00:00:00 2001 From: Bartek Kryza Date: Fri, 28 Apr 2023 22:46:36 +0200 Subject: [PATCH] WIP --- src/class_diagram/visitor/template_builder.cc | 189 ++++++++++++------ src/class_diagram/visitor/template_builder.h | 2 +- .../visitor/translation_unit_visitor.cc | 2 +- .../visitor/translation_unit_visitor.h | 4 + src/common/clang_utils.cc | 10 +- src/common/clang_utils.h | 2 + src/common/model/diagram_element.cc | 7 +- src/common/model/template_parameter.cc | 61 +++++- src/common/model/template_parameter.h | 35 +++- tests/CMakeLists.txt | 1 + tests/t00051/test_case.h | 12 +- tests/t00062/t00062.cc | 14 ++ tests/t00062/test_case.h | 2 +- 13 files changed, 254 insertions(+), 87 deletions(-) diff --git a/src/class_diagram/visitor/template_builder.cc b/src/class_diagram/visitor/template_builder.cc index 68611eea..e93f40a2 100644 --- a/src/class_diagram/visitor/template_builder.cc +++ b/src/class_diagram/visitor/template_builder.cc @@ -370,6 +370,21 @@ void template_builder::process_template_arguments( // arguments std::vector arguments; + // For now ignore the default template arguments to make the system + // templates 'nicer' - i.e. skipping the allocators and comparators + // TODO: Change this to ignore only when the arguments are set to + // default values, and add them when they are specifically + // overriden + const auto *maybe_type_parm_decl = + clang::dyn_cast( + template_decl->getTemplateParameters()->getParam( + std::min(arg_index, + template_decl->getTemplateParameters()->size() - 1))); + if (maybe_type_parm_decl && + maybe_type_parm_decl->hasDefaultArgument()) { + continue; + } + argument_process_dispatch(parent, cls, template_instantiation, template_decl, arg, arg_index, arguments); @@ -394,7 +409,7 @@ void template_builder::process_template_arguments( for (auto &argument : arguments) { simplify_system_template( - argument, argument.to_string(using_namespace(), false)); + argument, argument.to_string(using_namespace(), false, true)); LOG_DBG("Adding template argument {} to template " "specialization/instantiation {}", @@ -469,13 +484,19 @@ template_parameter template_builder::process_type_argument( auto type_name = common::to_string(arg, &cls->getASTContext()); - LOG_DBG("Processing template {} type argument: {}", - template_decl->getQualifiedNameAsString(), type_name); - auto argument = template_parameter::make_argument({}); - if (const auto *function_type = - arg.getAsType()->getAs(); + auto type = arg.getAsType().getNonReferenceType().getUnqualifiedType(); + if (type->isPointerType()) + type = type->getPointeeType(); +// if (type->isMemberPointerType()) +// type = type->getPointeeType(); + + LOG_DBG("Processing template {} type argument: {}, {}, {}", + template_decl->getQualifiedNameAsString(), type_name, + type->getTypeClassName(), common::to_string(type, cls->getASTContext())); + + if (const auto *function_type = type->getAs(); function_type != nullptr) { argument.set_function_template(true); @@ -553,7 +574,7 @@ template_parameter template_builder::process_type_argument( } } else if (const auto *nested_template_type = - arg.getAsType()->getAs(); + type->getAs(); nested_template_type != nullptr) { const auto nested_type_name = nested_template_type->getTemplateName() @@ -601,11 +622,10 @@ template_parameter template_builder::process_type_argument( diagram().add_class(std::move(nested_template_instantiation)); } } - else if (arg.getAsType()->getAs() != nullptr) { + else if (type->getAs() != nullptr) { argument = template_parameter::make_template_type({}); - auto parameter_name = - common::to_string(arg.getAsType(), cls->getASTContext()); + auto parameter_name = common::to_string(type, cls->getASTContext()); ensure_lambda_type_is_relative(parameter_name); @@ -617,14 +637,16 @@ template_parameter template_builder::process_type_argument( cls, parameter_name); if (maybe_arg) { - return *maybe_arg; + argument = *maybe_arg; } } - - argument.set_name(parameter_name); + else { + argument.set_name(parameter_name); + } } + /* // Case for unexposed template - else if ((type_name.find('<') != std::string::npos) || + else if ((type_name.find('<') != std::string::npos) && (type_name.find("type-parameter-") == 0)) { ensure_lambda_type_is_relative(type_name); @@ -660,14 +682,24 @@ template_parameter template_builder::process_type_argument( return template_parameter::make_template_type(type_name); } } - - else if (const auto maybe_arg = - get_template_argument_from_type_parameter_string( - cls, arg.getAsType().getAsString()); - maybe_arg) { - // The type is only in the form 'type-parameter-X-Y' so we have - // to match it to a template parameter name in the 'cls' template - argument = *maybe_arg; + */ + else if (type_name.find("type-parameter-") != std::string::npos) { + // This is some sort of template parameter with unexposed type + // parameters in the form 'type-parameter-X-Y' + if (const auto maybe_arg = + get_template_argument_from_type_parameter_string( + cls, arg.getAsType().getAsString()); + maybe_arg) { + // The type is only in the form 'type-parameter-X-Y' so we have + // to match it to a template parameter name in the 'cls' template + argument = *maybe_arg; + } + else { + // fallback - just put whatever clang returns + argument.is_template_parameter(false); + argument.set_type( + common::to_string(type, template_decl->getASTContext())); + } } else if (type_name.find("(lambda at ") == 0) { // This is just a lambda reference @@ -677,7 +709,26 @@ template_parameter template_builder::process_type_argument( else { // This is just a regular record type process_tag_argument( - template_instantiation, template_decl, arg, argument); + template_instantiation, template_decl, type, argument); + } + + if (arg.getAsType()->isLValueReferenceType()) { + argument.is_lvalue_reference(true); + } + if (arg.getAsType()->isRValueReferenceType()) { + argument.is_rvalue_reference(true); + } + if (arg.getAsType()->isPointerType()) { + argument.is_pointer(true); + } + if (arg.getAsType()->isMemberPointerType()) { + argument.set_method_template(true); + } + if (arg.getAsType().isConstQualified()) { + argument.set_qualifier(template_parameter::cvqualifier::kConst); + } + if (arg.getAsType().isVolatileQualified()) { + argument.set_qualifier(template_parameter::cvqualifier::kVolatile); } return argument; @@ -798,6 +849,7 @@ template_parameter map_type_parameter_to_template_parameter( std::string arg = tp; if (arg == "_Bool") arg = "bool"; + return template_parameter::make_argument(arg); } @@ -809,14 +861,12 @@ std::optional build_template_parameter( auto res = template_parameter::make_template_type({}); - std::string param_qualifier{}; // e.g. const& or && - auto it = begin; auto it_next = it; it_next++; if (*it == "const") { - param_qualifier = "const"; + res.set_qualifier(template_parameter::cvqualifier::kConst); it++; it_next++; } @@ -832,8 +882,8 @@ std::optional build_template_parameter( // template parameter with qualifier at the end else if (common::is_type_token(*it) && common::is_qualifier(*it_next)) { res = map_type_parameter_to_template_parameter(decl, *it); - param_qualifier += *it_next; - res.set_qualifier(param_qualifier); + // param_qualifier += *it_next; + // res.set_qualifier(param_qualifier); return res; } // method template parameter @@ -851,8 +901,8 @@ std::optional build_template_parameter( } if (it != end && common::is_qualifier(*it)) { - param_qualifier += *it; - res.set_qualifier(param_qualifier); + // param_qualifier += *it; + // res.set_qualifier(param_qualifier); } return res; @@ -884,6 +934,13 @@ std::optional build_template_parameter( // handle args if (*it == "(") { it++; + + if(it != end && *it == "...") { + // handle elipssis arg + res.is_elipssis(true); + return res; + } + while (true) { // This will break on more complex args auto arg_separator = std::find(it, end, ","); @@ -906,14 +963,15 @@ std::optional build_template_parameter( } } - assert(*it == ")"); + //if(it!=end) + //assert(*it == ")"); it++; - if (it != end && common::is_qualifier(*it)) { - param_qualifier += *it; - res.set_qualifier(param_qualifier); - } + //if (it != end && common::is_qualifier(*it)) { + // param_qualifier += *it; + // res.set_qualifier(param_qualifier); + //} } return res; @@ -941,8 +999,11 @@ template_parameter template_builder::process_integral_argument( { assert(arg.getKind() == clang::TemplateArgument::Integral); - return template_parameter::make_argument( - std::to_string(arg.getAsIntegral().getExtValue())); + std::string result; + llvm::raw_string_ostream ostream(result); + arg.dump(ostream); + + return template_parameter::make_argument(result); } template_parameter template_builder::process_null_argument( @@ -993,56 +1054,70 @@ std::vector template_builder::process_pack_argument( } void template_builder::process_tag_argument(class_ &template_instantiation, - const clang::TemplateDecl *template_decl, - const clang::TemplateArgument &arg, template_parameter &argument) + const clang::TemplateDecl *template_decl, const clang::QualType type, + template_parameter &argument) { - assert(arg.getKind() == clang::TemplateArgument::Type); + [[maybe_unused]] auto current_instantiation_name = + template_instantiation.full_name(false); argument.is_template_parameter(false); + auto type_name = common::to_string(type, template_decl->getASTContext()); + argument.set_type(type_name); + const auto type_id = common::to_id(type_name); + argument.set_id(type_id); - argument.set_type( - common::to_string(arg.getAsType(), template_decl->getASTContext())); - - if (const auto *tsp = - arg.getAsType()->getAs(); + if (const auto *tsp = type->getAs(); tsp != nullptr) { if (const auto *record_type_decl = tsp->getAsRecordDecl(); record_type_decl != nullptr) { - argument.set_id(common::to_id(arg)); if (diagram().should_include( template_decl->getQualifiedNameAsString())) { // Add dependency relationship to the parent // template template_instantiation.add_relationship( - {relationship_t::kDependency, common::to_id(arg)}); + {relationship_t::kDependency, type_id}); } } } - else if (const auto *record_type = - arg.getAsType()->getAs(); + else if (const auto *record_type = type->getAs(); record_type != nullptr) { - if (const auto *record_type_decl = record_type->getAsRecordDecl(); - record_type_decl != nullptr) { - argument.set_id(common::to_id(arg)); + const auto *class_template_specialization = + clang::dyn_cast( + record_type->getAsRecordDecl()); + + if (class_template_specialization != nullptr) { + auto tag_argument = build_from_class_template_specialization( + *class_template_specialization); + + if (tag_argument) { + argument.set_type(tag_argument->name_and_ns()); + for (const auto &p : tag_argument->template_params()) + argument.add_template_param(p); + for (auto &r : tag_argument->relationships()) { + template_instantiation.add_relationship(std::move(r)); + } + } + } + else if (const auto *record_type_decl = record_type->getAsRecordDecl(); + record_type_decl != nullptr) { #if LLVM_VERSION_MAJOR >= 16 argument.set_type(record_type_decl->getQualifiedNameAsString()); #endif - if (diagram().should_include( - template_decl->getQualifiedNameAsString())) { + if (diagram().should_include(type_name)) { // Add dependency relationship to the parent // template template_instantiation.add_relationship( - {relationship_t::kDependency, common::to_id(arg)}); + {relationship_t::kDependency, type_id}); } } } - else if (const auto *enum_type = arg.getAsType()->getAs(); + else if (const auto *enum_type = type->getAs(); enum_type != nullptr) { if (enum_type->getAsTagDecl() != nullptr) { template_instantiation.add_relationship( - {relationship_t::kDependency, common::to_id(arg)}); + {relationship_t::kDependency, type_id}); } } } diff --git a/src/class_diagram/visitor/template_builder.h b/src/class_diagram/visitor/template_builder.h index 5f3a2cd1..0c8b14ca 100644 --- a/src/class_diagram/visitor/template_builder.h +++ b/src/class_diagram/visitor/template_builder.h @@ -82,7 +82,7 @@ public: void process_tag_argument(model::class_ &template_instantiation, const clang::TemplateDecl *template_decl, - const clang::TemplateArgument &arg, + const clang::QualType type, common::model::template_parameter &argument); template_parameter process_expression_argument( diff --git a/src/class_diagram/visitor/translation_unit_visitor.cc b/src/class_diagram/visitor/translation_unit_visitor.cc index 9cff7ec3..7f957783 100644 --- a/src/class_diagram/visitor/translation_unit_visitor.cc +++ b/src/class_diagram/visitor/translation_unit_visitor.cc @@ -1875,7 +1875,7 @@ void translation_unit_visitor::process_field( template_specialization.template_params()) { LOG_DBG("Looking for nested relationships from {}::{} in " - "template {}", + "template argument {}", c.full_name(false), field_name, template_argument.to_string( config().using_namespace(), false)); diff --git a/src/class_diagram/visitor/translation_unit_visitor.h b/src/class_diagram/visitor/translation_unit_visitor.h index 2af05e0a..01b98387 100644 --- a/src/class_diagram/visitor/translation_unit_visitor.h +++ b/src/class_diagram/visitor/translation_unit_visitor.h @@ -67,6 +67,10 @@ public: clanguml::class_diagram::model::diagram &diagram, const clanguml::config::class_diagram &config); + bool shouldVisitTemplateInstantiations() const { return false; } + + bool shouldVisitImplicitCode() const { return false; } + virtual bool VisitNamespaceDecl(clang::NamespaceDecl *ns); virtual bool VisitRecordDecl(clang::RecordDecl *D); diff --git a/src/common/clang_utils.cc b/src/common/clang_utils.cc index 874a4812..bb03f840 100644 --- a/src/common/clang_utils.cc +++ b/src/common/clang_utils.cc @@ -306,6 +306,10 @@ template <> id_t to_id(const std::string &full_name) return static_cast(std::hash{}(full_name) >> 3U); } +id_t to_id(const clang::QualType &type, const clang::ASTContext &ctx) { + return to_id(common::to_string(type, ctx)); +} + template <> id_t to_id(const clang::NamespaceDecl &declaration) { return to_id(get_qualified_name(declaration)); @@ -533,7 +537,7 @@ std::vector tokenize_unexposed_template_parameter( std::string tok; for (const char c : word) { - if (c == '(' || c == ')' || c == '[' || c == ']') { + if (c == '(' || c == ')' || c == '[' || c == ']' || c == '<' || c == '>') { if (!tok.empty()) result.push_back(tok); result.push_back(std::string{c}); @@ -544,6 +548,10 @@ std::vector tokenize_unexposed_template_parameter( result.push_back(tok); tok = ":"; } + else if (tok == ":") { + result.push_back("::"); + tok = ""; + } else { tok += ':'; } diff --git a/src/common/clang_utils.h b/src/common/clang_utils.h index 0c643692..32218304 100644 --- a/src/common/clang_utils.h +++ b/src/common/clang_utils.h @@ -129,6 +129,8 @@ template id_t to_id(const T &declaration); template <> id_t to_id(const std::string &full_name); +id_t to_id(const clang::QualType &type, const clang::ASTContext &ctx); + template <> id_t to_id(const clang::NamespaceDecl &declaration); template <> id_t to_id(const clang::CXXRecordDecl &declaration); diff --git a/src/common/model/diagram_element.cc b/src/common/model/diagram_element.cc index e8392e28..bf76c0c4 100644 --- a/src/common/model/diagram_element.cc +++ b/src/common/model/diagram_element.cc @@ -46,11 +46,12 @@ void diagram_element::add_relationship(relationship &&cr) return; } - LOG_DBG("Adding relationship from: '{}' ({}) - {} - '{}'", id(), - full_name(true), to_string(cr.type()), cr.destination()); + if (!util::contains(relationships_, cr)) { + LOG_DBG("Adding relationship from: '{}' ({}) - {} - '{}'", id(), + full_name(true), to_string(cr.type()), cr.destination()); - if (!util::contains(relationships_, cr)) relationships_.emplace_back(std::move(cr)); + } } std::vector &diagram_element::relationships() diff --git a/src/common/model/template_parameter.cc b/src/common/model/template_parameter.cc index 7ed55175..52457da0 100644 --- a/src/common/model/template_parameter.cc +++ b/src/common/model/template_parameter.cc @@ -113,7 +113,10 @@ int template_parameter::calculate_specialization_match( { int res{0}; - if (qualifier() != base_template_parameter.qualifier()) + // If the potential base template has a qualifier, the current template + // must match it + if (!base_template_parameter.qualifiers().empty() && + (qualifiers() != base_template_parameter.qualifiers())) return 0; if (is_template_parameter() && @@ -211,10 +214,33 @@ bool operator!=(const template_parameter &l, const template_parameter &r) return !(l == r); } -std::string template_parameter::to_string( - const clanguml::common::model::namespace_ &using_namespace, - bool relative) const +std::string template_parameter::qualifiers_str() const { + std::string res; + if (qualifiers_.count(cvqualifier::kConst) == 1) + res += "const"; + + if (is_rvalue_reference()) + res += "&&"; + else if (is_lvalue_reference()) + res += "&"; + + if (is_pointer()) + res += "*"; + + if(res.empty()) + return res; + + return " " + res; +} + +std::string template_parameter::to_string( + const clanguml::common::model::namespace_ &using_namespace, bool relative, + bool skip_qualifiers) const +{ + if(is_elipssis()) + return "..."; + using clanguml::common::model::namespace_; assert(!(type().has_value() && concept_constraint().has_value())); @@ -235,11 +261,11 @@ std::string template_parameter::to_string( if (is_method_template()) { assert(template_params().size() > 1); + std::string unqualified; if (template_params().size() == 2) { - return fmt::format("{} {}::*{}", + unqualified = fmt::format("{} {}::*", template_params().at(0).to_string(using_namespace, relative), - template_params().at(1).to_string(using_namespace, relative), - qualifier()); + template_params().at(1).to_string(using_namespace, relative)); } else { auto it = template_params().begin(); @@ -253,9 +279,14 @@ std::string template_parameter::to_string( args.push_back(it->to_string(using_namespace, relative)); } - return fmt::format("{} ({}::*)({}){}", return_type, class_type, - fmt::join(args, ","), qualifier()); + unqualified = fmt::format("{} ({}::*)({})", return_type, class_type, + fmt::join(args, ",")); } + + if (skip_qualifiers) + return unqualified; + else + return unqualified + qualifiers_str(); } std::string res; @@ -307,8 +338,8 @@ std::string template_parameter::to_string( res += fmt::format("<{}>", fmt::join(params, ",")); } - if (!qualifier().empty()) - res += " " + qualifier(); + if (!skip_qualifiers) + res += qualifiers_str(); const auto &maybe_default_value = default_value(); if (maybe_default_value) { @@ -332,6 +363,9 @@ bool template_parameter::find_nested_relationships( // just add it and skip recursion (e.g. this is a user defined type) const auto maybe_type = type(); if (maybe_type && should_include(maybe_type.value())) { + if (is_pointer() || is_lvalue_reference() || is_rvalue_reference()) + hint = common::model::relationship_t::kAssociation; + const auto maybe_id = id(); if (maybe_id) { nested_relationships.emplace_back(maybe_id.value(), hint); @@ -349,6 +383,11 @@ bool template_parameter::find_nested_relationships( if (maybe_id && maybe_arg_type && should_include(*maybe_arg_type)) { + if (template_argument.is_pointer() || + template_argument.is_lvalue_reference() || + template_argument.is_rvalue_reference()) + hint = common::model::relationship_t::kAssociation; + nested_relationships.emplace_back(maybe_id.value(), hint); added_aggregation_relationship = diff --git a/src/common/model/template_parameter.h b/src/common/model/template_parameter.h index 0811dca6..cf17a2e1 100644 --- a/src/common/model/template_parameter.h +++ b/src/common/model/template_parameter.h @@ -21,6 +21,7 @@ #include "common/model/namespace.h" #include +#include #include #include @@ -44,6 +45,13 @@ std::string to_string(template_parameter_kind_t k); /// nested templates class template_parameter { public: + enum class cvqualifier { + kConst, + kVolatile, + kLValueReference, + kRValueReference + }; + static template_parameter make_template_type(const std::string &name, const std::optional &default_value = {}, bool is_variadic = false) @@ -149,7 +157,7 @@ public: std::string to_string( const clanguml::common::model::namespace_ &using_namespace, - bool relative) const; + bool relative, bool skip_qualifiers = false) const; void add_template_param(template_parameter &&ct); @@ -186,13 +194,26 @@ public: bool is_method_template() const { return is_method_template_; } - void set_qualifier(const std::string &q) { qualifier_ = q; } + void set_qualifier(const cvqualifier q) { qualifiers_.emplace(q); } - const std::string &qualifier() const { return qualifier_; } + const std::set &qualifiers() const { return qualifiers_; } + void is_pointer(bool p) { is_pointer_ = p; } + bool is_pointer() const { return is_pointer_; } + + void is_lvalue_reference(bool p) { is_lvalue_reference_ = p; } + bool is_lvalue_reference() const { return is_lvalue_reference_; } + + void is_rvalue_reference(bool p) { is_rvalue_reference_ = p; } + bool is_rvalue_reference() const { return is_rvalue_reference_; } + + void is_elipssis(bool e) { is_elipssis_ = e; } + bool is_elipssis() const { return is_elipssis_; } private: template_parameter() = default; + std::string qualifiers_str() const; + template_parameter_kind_t kind_{template_parameter_kind_t::template_type}; /// Represents the type of non-type template parameters @@ -213,14 +234,20 @@ private: /// Can only be true when is_template_parameter_ is true bool is_template_template_parameter_{false}; + bool is_elipssis_{false}; + /// Whether the template parameter is variadic bool is_variadic_{false}; + bool is_pointer_{false}; + bool is_lvalue_reference_{false}; + bool is_rvalue_reference_{false}; + bool is_function_template_{false}; bool is_method_template_{false}; - std::string qualifier_; + std::set qualifiers_; /// Stores optional fully qualified name of constraint for this template /// parameter diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4c17b96e..9491e375 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -40,6 +40,7 @@ set(TEST_CASES test_config test_cli_handler test_filters + test_template_parser test_thread_pool_executor) foreach(TEST_NAME ${TEST_CASES}) diff --git a/tests/t00051/test_case.h b/tests/t00051/test_case.h index 6fb6d73a..a19d6067 100644 --- a/tests/t00051/test_case.h +++ b/tests/t00051/test_case.h @@ -60,20 +60,16 @@ TEST_CASE("t00051", "[test-case][class]") REQUIRE_THAT(puml, IsClassTemplate("B", - "(lambda at ../../tests/t00051/t00051.cc:43:18),(lambda at " - "../../tests/t00051/t00051.cc:43:27)")); + "(lambda at ../../tests/t00051/t00051.cc:43:18)")); + //,(lambda at ../../tests/t00051/t00051.cc:43:27)")); REQUIRE_THAT(puml, IsInstantiation(_A("B"), - _A("B<(lambda at ../../tests/t00051/t00051.cc:43:18),(lambda " - "at " - "../../tests/t00051/t00051.cc:43:27)>"))); + _A("B<(lambda at ../../tests/t00051/t00051.cc:43:18)>"))); REQUIRE_THAT(puml, IsDependency(_A("A"), - _A("B<(lambda at ../../tests/t00051/t00051.cc:43:18),(lambda " - "at " - "../../tests/t00051/t00051.cc:43:27)>"))); + _A("B<(lambda at ../../tests/t00051/t00051.cc:43:18)>"))); save_puml( config.output_directory() + "/" + diagram->name + ".puml", puml); diff --git a/tests/t00062/t00062.cc b/tests/t00062/t00062.cc index d9ccd5ef..7858d7f2 100644 --- a/tests/t00062/t00062.cc +++ b/tests/t00062/t00062.cc @@ -1,3 +1,5 @@ +#include +#include #include namespace clanguml { @@ -8,6 +10,17 @@ template struct A { U &u; }; +template struct A &> { + U &u; +}; + +template <> +struct A> &> { }; + +template struct A { + U ***u; +}; + template struct A { U &&u; }; @@ -58,5 +71,6 @@ template struct A { template <> struct A { std::vector n; }; + } } \ No newline at end of file diff --git a/tests/t00062/test_case.h b/tests/t00062/test_case.h index 335b43a2..194020cc 100644 --- a/tests/t00062/test_case.h +++ b/tests/t00062/test_case.h @@ -36,7 +36,7 @@ TEST_CASE("t00062", "[test-case][class]") REQUIRE_THAT(puml, EndsWith("@enduml\n")); // Check if all classes exist - REQUIRE_THAT(puml, IsClassTemplate("A", "T")); + REQUIRE_THAT(puml, IsClassTemplate("A", "TTTT")); REQUIRE_THAT(puml, IsClassTemplate("A", "U &")); REQUIRE_THAT(puml, IsClassTemplate("A", "U &&")); REQUIRE_THAT(puml, IsClassTemplate("A", "U const&"));