diff --git a/README.md b/README.md index addaa08e..e8eb761d 100644 --- a/README.md +++ b/README.md @@ -439,6 +439,7 @@ This project relies on the following great tools: * [inja](https://github.com/pantor/inja) - a template engine for modern C++ * [backward-cpp](https://github.com/bombela/backward-cpp) - stack trace pretty printer for C++ * [yaml-cpp](https://github.com/jbeder/yaml-cpp) - YAML parser library for C++ +* [spdlog](https://github.com/gabime/spdlog) - Fast C++ logging library ## Contributing diff --git a/src/class_diagram/generators/plantuml/class_diagram_generator.cc b/src/class_diagram/generators/plantuml/class_diagram_generator.cc index 929a06fc..fbb88494 100644 --- a/src/class_diagram/generators/plantuml/class_diagram_generator.cc +++ b/src/class_diagram/generators/plantuml/class_diagram_generator.cc @@ -123,10 +123,6 @@ void generator::generate(const class_ &c, std::ostream &ostr) const { namespace plantuml_common = clanguml::common::generators::plantuml; - constexpr auto kAbbreviatedMethodArgumentsLength{15}; - - const auto &uns = m_config.using_namespace(); - std::string class_type{"class"}; if (c.is_abstract()) class_type = "abstract"; @@ -149,74 +145,11 @@ void generator::generate(const class_ &c, std::ostream &ostr) const // // Process methods // - for (const auto &m : c.methods()) { - if (!m_model.should_include(m.access())) - continue; - - print_debug(m, ostr); - - if (m.is_pure_virtual()) - ostr << "{abstract} "; - - if (m.is_static()) - ostr << "{static} "; - - std::string type{ - uns.relative(m_config.simplify_template_type(m.type()))}; - - ostr << plantuml_common::to_plantuml(m.access()) << m.name(); - - if (!m.template_params().empty()) { - m.render_template_params(ostr, m_config.using_namespace(), false); - } - - ostr << "("; - if (m_config.generate_method_arguments() != - config::method_arguments::none) { - std::vector params; - std::transform(m.parameters().cbegin(), m.parameters().cend(), - std::back_inserter(params), [this](const auto &mp) { - return m_config.simplify_template_type( - mp.to_string(m_config.using_namespace())); - }); - auto args_string = fmt::format("{}", fmt::join(params, ", ")); - if (m_config.generate_method_arguments() == - config::method_arguments::abbreviated) { - args_string = clanguml::util::abbreviate( - args_string, kAbbreviatedMethodArgumentsLength); - } - ostr << args_string; - } - ostr << ")"; - - if (m.is_constexpr()) - ostr << " constexpr"; - else if (m.is_consteval()) - ostr << " consteval"; - - if (m.is_const()) - ostr << " const"; - - if (m.is_noexcept()) - ostr << " noexcept"; - - assert(!(m.is_pure_virtual() && m.is_defaulted())); - - if (m.is_pure_virtual()) - ostr << " = 0"; - - if (m.is_defaulted()) - ostr << " = default"; - else if (m.is_deleted()) - ostr << " = deleted"; - - ostr << " : " << type; - - if (m_config.generate_links) { - generate_link(ostr, m); - } - - ostr << '\n'; + if (m_config.group_methods()) { + generate_methods(group_methods(c.methods()), ostr); + } + else { + generate_methods(c.methods(), ostr); } // @@ -226,52 +159,33 @@ void generator::generate(const class_ &c, std::ostream &ostr) const std::set rendered_relations; std::stringstream all_relations_str; - std::set unique_relations; for (const auto &r : c.relationships()) { if (!m_model.should_include(r.type())) continue; - LOG_DBG("Processing relationship {}", - plantuml_common::to_plantuml(r.type(), r.style())); - - std::string destination; try { - auto target_element = m_model.get(r.destination()); - if (!target_element.has_value()) - throw error::uml_alias_missing{ - fmt::format("Missing element in the model for ID: {}", - r.destination())}; - - destination = target_element.value().full_name(false); - - if (util::starts_with(destination, std::string{"::"})) - destination = destination.substr(2, destination.size()); - - std::string puml_relation; - if (!r.multiplicity_source().empty()) - puml_relation += "\"" + r.multiplicity_source() + "\" "; - - puml_relation += plantuml_common::to_plantuml(r.type(), r.style()); - - if (!r.multiplicity_destination().empty()) - puml_relation += " \"" + r.multiplicity_destination() + "\""; - - if (!r.label().empty()) { - rendered_relations.emplace(r.label()); - } + generate_relationship(r, rendered_relations, ostr); } catch (error::uml_alias_missing &e) { LOG_DBG("Skipping {} relation from {} to {} due " "to: {}", plantuml_common::to_plantuml(r.type(), r.style()), - c.full_name(), destination, e.what()); + c.full_name(), r.destination(), e.what()); } } // // Process members // - for (const auto &m : c.members()) { + std::vector members{ + c.members()}; + + sort_class_elements(members); + + if (m_config.group_methods()) + ostr << "__\n"; + + for (const auto &m : members) { if (!m_model.should_include(m.access())) continue; @@ -279,18 +193,7 @@ void generator::generate(const class_ &c, std::ostream &ostr) const rendered_relations.find(m.name()) != rendered_relations.end()) continue; - print_debug(m, ostr); - - if (m.is_static()) - ostr << "{static} "; - - ostr << plantuml_common::to_plantuml(m.access()) << m.name() << " : " - << render_name( - uns.relative(m_config.simplify_template_type(m.type()))); - - if (m_config.generate_links) { - generate_link(ostr, m); - } + generate_member(m, ostr); ostr << '\n'; } @@ -306,6 +209,162 @@ void generator::generate(const class_ &c, std::ostream &ostr) const generate_member_notes(ostr, method, c.alias()); } +void generator::generate_methods( + const method_groups_t &methods, std::ostream &ostr) const +{ + bool is_first_non_empty_group{true}; + + for (const auto &group : method_groups_) { + const auto &group_methods = methods.at(group); + if (!group_methods.empty()) { + if (!is_first_non_empty_group) + ostr << "..\n"; + is_first_non_empty_group = false; + generate_methods(group_methods, ostr); + } + } +} + +void generator::generate_methods( + const std::vector &methods, std::ostream &ostr) const +{ + auto sorted_methods = methods; + sort_class_elements(sorted_methods); + + for (const auto &m : sorted_methods) { + if (!m_model.should_include(m.access())) + continue; + + generate_method(m, ostr); + + ostr << '\n'; + } +} + +generator::method_groups_t generator::group_methods( + const std::vector &methods) const +{ + std::map> result; + + // First get rid of methods which don't pass the filters + std::vector filtered_methods; + std::copy_if(methods.cbegin(), methods.cend(), + std::back_inserter(filtered_methods), + [this](auto &m) { return m_model.should_include(m.access()); }); + + for (const auto &g : method_groups_) { + result[g] = {}; + } + + for (const auto &m : filtered_methods) { + if (m.is_constructor() || m.is_destructor()) { + result["constructors"].push_back(m); + } + else if (m.is_copy_assignment() || m.is_move_assignment()) { + result["assignment"].push_back(m); + } + else if (m.is_operator()) { + result["operators"].push_back(m); + } + else { + result["other"].push_back(m); + } + } + + return result; +} + +void generator::generate_method( + const class_diagram::model::class_method &m, std::ostream &ostr) const +{ + namespace plantuml_common = clanguml::common::generators::plantuml; + const auto &uns = m_config.using_namespace(); + + constexpr auto kAbbreviatedMethodArgumentsLength{15}; + + print_debug(m, ostr); + + if (m.is_pure_virtual()) + ostr << "{abstract} "; + + if (m.is_static()) + ostr << "{static} "; + + std::string type{uns.relative(m_config.simplify_template_type(m.type()))}; + + ostr << plantuml_common::to_plantuml(m.access()) << m.name(); + + if (!m.template_params().empty()) { + m.render_template_params(ostr, m_config.using_namespace(), false); + } + + ostr << "("; + if (m_config.generate_method_arguments() != + config::method_arguments::none) { + std::vector params; + std::transform(m.parameters().cbegin(), m.parameters().cend(), + std::back_inserter(params), [this](const auto &mp) { + return m_config.simplify_template_type( + mp.to_string(m_config.using_namespace())); + }); + auto args_string = fmt::format("{}", fmt::join(params, ", ")); + if (m_config.generate_method_arguments() == + config::method_arguments::abbreviated) { + args_string = clanguml::util::abbreviate( + args_string, kAbbreviatedMethodArgumentsLength); + } + ostr << args_string; + } + ostr << ")"; + + if (m.is_constexpr()) + ostr << " constexpr"; + else if (m.is_consteval()) + ostr << " consteval"; + + if (m.is_const()) + ostr << " const"; + + if (m.is_noexcept()) + ostr << " noexcept"; + + assert(!(m.is_pure_virtual() && m.is_defaulted())); + + if (m.is_pure_virtual()) + ostr << " = 0"; + + if (m.is_defaulted()) + ostr << " = default"; + else if (m.is_deleted()) + ostr << " = deleted"; + + ostr << " : " << type; + + if (m_config.generate_links) { + generate_link(ostr, m); + } +} + +void generator::generate_member( + const class_diagram::model::class_member &m, std::ostream &ostr) const +{ + namespace plantuml_common = clanguml::common::generators::plantuml; + const auto &uns = m_config.using_namespace(); + + print_debug(m, ostr); + + if (m.is_static()) + ostr << "{static} "; + + ostr << plantuml_common::to_plantuml(m.access()) << m.name() << " : " + << render_name( + uns.relative(m_config.simplify_template_type(m.type()))); + + if (m_config.generate_links) { + generate_link(ostr, m); + } +} + void generator::generate(const concept_ &c, std::ostream &ostr) const { std::string class_type{"class"}; @@ -377,6 +436,40 @@ void generator::generate_relationships(std::ostream &ostr) const } } +void generator::generate_relationship(const relationship &r, + std::set &rendered_relations, std::ostream &ostr) const +{ + namespace plantuml_common = clanguml::common::generators::plantuml; + + LOG_DBG("Processing relationship {}", + plantuml_common::to_plantuml(r.type(), r.style())); + + std::string destination; + + auto target_element = m_model.get(r.destination()); + if (!target_element.has_value()) + throw error::uml_alias_missing{fmt::format( + "Missing element in the model for ID: {}", r.destination())}; + + destination = target_element.value().full_name(false); + + if (util::starts_with(destination, std::string{"::"})) + destination = destination.substr(2, destination.size()); + + std::string puml_relation; + if (!r.multiplicity_source().empty()) + puml_relation += "\"" + r.multiplicity_source() + "\" "; + + puml_relation += plantuml_common::to_plantuml(r.type(), r.style()); + + if (!r.multiplicity_destination().empty()) + puml_relation += " \"" + r.multiplicity_destination() + "\""; + + if (!r.label().empty()) { + rendered_relations.emplace(r.label()); + } +} + void generator::generate_relationships( const class_ &c, std::ostream &ostr) const { @@ -732,8 +825,8 @@ void generator::generate_relationships( for (const auto &subpackage : p) { if (dynamic_cast(subpackage.get()) != nullptr) { // TODO: add option - generate_empty_packages, currently - // packages which do not contain anything but other packages - // are skipped + // packages which do not contain anything but other + // packages are skipped const auto &sp = dynamic_cast(*subpackage); if (!sp.is_empty() && !sp.all_of([this](const common::model::element &e) { diff --git a/src/class_diagram/generators/plantuml/class_diagram_generator.h b/src/class_diagram/generators/plantuml/class_diagram_generator.h index 4929c7c1..86b293a7 100644 --- a/src/class_diagram/generators/plantuml/class_diagram_generator.h +++ b/src/class_diagram/generators/plantuml/class_diagram_generator.h @@ -48,20 +48,24 @@ using common_generator = using clanguml::class_diagram::model::class_; using clanguml::class_diagram::model::class_element; +using clanguml::class_diagram::model::class_member; +using clanguml::class_diagram::model::class_method; using clanguml::class_diagram::model::concept_; using clanguml::class_diagram::model::enum_; using clanguml::common::model::access_t; using clanguml::common::model::package; +using clanguml::common::model::relationship; using clanguml::common::model::relationship_t; using namespace clanguml::util; class generator : public common_generator { + using method_groups_t = std::map>; + public: generator(diagram_config &config, diagram_model &model); - void generate_link( - std::ostream &ostr, const class_diagram::model::class_element &e) const; + void generate_link(std::ostream &ostr, const class_element &e) const; void generate_alias(const class_ &c, std::ostream &ostr) const; @@ -71,12 +75,25 @@ public: void generate(const class_ &c, std::ostream &ostr) const; + void generate_methods( + const std::vector &methods, std::ostream &ostr) const; + + void generate_methods( + const method_groups_t &methods, std::ostream &ostr) const; + + void generate_method(const class_method &m, std::ostream &ostr) const; + + void generate_member(const class_member &m, std::ostream &ostr) const; + void generate_top_level_elements(std::ostream &ostr) const; void generate_relationships(std::ostream &ostr) const; void generate_relationships(const class_ &c, std::ostream &ostr) const; + void generate_relationship(const relationship &r, + std::set &rendered_relations, std::ostream &ostr) const; + void generate(const enum_ &e, std::ostream &ostr) const; void generate_relationships(const enum_ &c, std::ostream &ostr) const; @@ -96,9 +113,26 @@ public: void generate(std::ostream &ostr) const override; + method_groups_t group_methods( + const std::vector &methods) const; + private: + const std::vector method_groups_{ + "constructors", "assignment", "operators", "other"}; + std::string render_name(std::string name) const; + template + void sort_class_elements(std::vector &elements) const + { + if (m_config.member_order() == config::member_order_t::lexical) { + std::sort(elements.begin(), elements.end(), + [](const auto &m1, const auto &m2) { + return m1.name() < m2.name(); + }); + } + } + mutable common::generators::nested_element_stack together_group_stack_; }; diff --git a/src/class_diagram/model/class_method.cc b/src/class_diagram/model/class_method.cc index 0044026c..9a2a5097 100644 --- a/src/class_diagram/model/class_method.cc +++ b/src/class_diagram/model/class_method.cc @@ -81,6 +81,13 @@ void class_method::is_constructor(bool is_constructor) is_constructor_ = is_constructor; } +bool class_method::is_destructor() const { return is_destructor_; } + +void class_method::is_destructor(bool is_destructor) +{ + is_destructor_ = is_destructor; +} + bool class_method::is_move_assignment() const { return is_move_assignment_; } void class_method::is_move_assignment(bool is_move_assignment) diff --git a/src/class_diagram/model/class_method.h b/src/class_diagram/model/class_method.h index 1e44aff0..7e07d9f8 100644 --- a/src/class_diagram/model/class_method.h +++ b/src/class_diagram/model/class_method.h @@ -66,6 +66,9 @@ public: bool is_constructor() const; void is_constructor(bool is_constructor); + bool is_destructor() const; + void is_destructor(bool is_destructor); + bool is_move_assignment() const; void is_move_assignment(bool is_move_assignment); @@ -91,6 +94,7 @@ private: bool is_constexpr_{false}; bool is_consteval_{false}; bool is_constructor_{false}; + bool is_destructor_{false}; bool is_move_assignment_{false}; bool is_copy_assignment_{false}; bool is_operator_{false}; diff --git a/src/class_diagram/visitor/translation_unit_visitor.cc b/src/class_diagram/visitor/translation_unit_visitor.cc index 3e897a14..ae8d0b25 100644 --- a/src/class_diagram/visitor/translation_unit_visitor.cc +++ b/src/class_diagram/visitor/translation_unit_visitor.cc @@ -1284,6 +1284,7 @@ void translation_unit_visitor::process_method( util::trim(method_name), method_return_type}; const bool is_constructor = c.name() == method_name; + const bool is_destructor = fmt::format("~{}", c.name()) == method_name; method.is_pure_virtual(mf.isPure()); method.is_virtual(mf.isVirtual()); @@ -1295,6 +1296,7 @@ void translation_unit_visitor::process_method( method.is_constexpr(mf.isConstexprSpecified() && !is_constructor); method.is_consteval(mf.isConsteval()); method.is_constructor(is_constructor); + method.is_destructor(is_destructor); method.is_move_assignment(mf.isMoveAssignmentOperator()); method.is_copy_assignment(mf.isCopyAssignmentOperator()); method.is_noexcept(isNoexceptExceptionSpec(mf.getExceptionSpecType())); diff --git a/src/config/config.h b/src/config/config.h index e728a66c..c0a0acc3 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -39,6 +39,8 @@ enum class method_arguments { full, abbreviated, none }; enum class package_type_t { kNamespace, kDirectory }; +enum class member_order_t { lexical, as_is }; + std::string to_string(method_arguments ma); enum class comment_parser_t { plain, clang }; @@ -163,6 +165,9 @@ struct inheritable_diagram_options { option puml{"plantuml", option_inherit_mode::kAppend}; option generate_method_arguments{ "generate_method_arguments", method_arguments::full}; + option group_methods{"group_methods", true}; + option member_order{ + "method_order", member_order_t::lexical}; option generate_packages{"generate_packages", false}; option package_type{ "package_type", package_type_t::kNamespace}; diff --git a/src/config/yaml_decoders.cc b/src/config/yaml_decoders.cc index fc885031..de3d6f32 100644 --- a/src/config/yaml_decoders.cc +++ b/src/config/yaml_decoders.cc @@ -32,6 +32,7 @@ using clanguml::config::hint_t; using clanguml::config::include_diagram; using clanguml::config::layout_hint; using clanguml::config::location_t; +using clanguml::config::member_order_t; using clanguml::config::method_arguments; using clanguml::config::package_diagram; using clanguml::config::package_type_t; @@ -89,6 +90,21 @@ void get_option( } } +template <> +void get_option( + const Node &node, clanguml::config::option &option) +{ + if (node[option.name]) { + const auto &val = node[option.name].as(); + if (val == "as_is") + option.set(member_order_t::as_is); + else if (val == "lexical") + option.set(member_order_t::lexical); + else + throw std::runtime_error("Invalid member_order value: " + val); + } +} + template <> void get_option( const Node &node, clanguml::config::option &option) @@ -139,7 +155,6 @@ void get_option>( YAML::Node included_node = YAML::LoadFile(include_path.string()); - // diagram_config = parse_diagram_config(included_node); option.set( included_node.as< std::map>()); @@ -419,6 +434,8 @@ template <> struct convert { get_option(node, rhs.layout); get_option(node, rhs.include_relations_also_as_members); get_option(node, rhs.generate_method_arguments); + get_option(node, rhs.group_methods); + get_option(node, rhs.member_order); get_option(node, rhs.generate_packages); get_option(node, rhs.package_type); get_option(node, rhs.relationship_hints); diff --git a/tests/test_cases.cc b/tests/test_cases.cc index b4b92bd9..79bbf3f0 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -304,6 +304,7 @@ using namespace clanguml::test::matchers; #if defined(ENABLE_CXX_STD_20_TEST_CASES) #include "t00065/test_case.h" #endif +#include "t00066/test_case.h" /// /// Sequence diagram tests diff --git a/tests/test_cases.yaml b/tests/test_cases.yaml index a2360d57..b341608f 100644 --- a/tests/test_cases.yaml +++ b/tests/test_cases.yaml @@ -192,6 +192,8 @@ test_cases: - name: t00065 title: Class diagram with packages from directory structure description: + - name: t00066 + title: Class fields and methods without grouping and sorting Sequence diagrams: - name: t20001 title: Basic sequence diagram test case