Added default class method and member grouping and sorting

This commit is contained in:
Bartek Kryza
2023-05-31 22:46:39 +02:00
parent 097f7a11ed
commit c87bd7d94b
10 changed files with 285 additions and 119 deletions

View File

@@ -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

View File

@@ -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<std::string> 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<std::string> rendered_relations;
std::stringstream all_relations_str;
std::set<std::string> 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<clanguml::class_diagram::model::class_member> 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<class_method> &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<class_method> &methods) const
{
std::map<std::string, std::vector<class_method>> result;
// First get rid of methods which don't pass the filters
std::vector<class_method> 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<std::string> 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<std::string> &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<package *>(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<package &>(*subpackage);
if (!sp.is_empty() &&
!sp.all_of([this](const common::model::element &e) {

View File

@@ -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<diagram_config, diagram_model> {
using method_groups_t = std::map<std::string, std::vector<class_method>>;
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<class_method> &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<std::string> &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<class_method> &methods) const;
private:
const std::vector<std::string> method_groups_{
"constructors", "assignment", "operators", "other"};
std::string render_name(std::string name) const;
template <typename T>
void sort_class_elements(std::vector<T> &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<common::model::element>
together_group_stack_;
};

View File

@@ -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)

View File

@@ -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};

View File

@@ -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()));

View File

@@ -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<plantuml> puml{"plantuml", option_inherit_mode::kAppend};
option<method_arguments> generate_method_arguments{
"generate_method_arguments", method_arguments::full};
option<bool> group_methods{"group_methods", true};
option<member_order_t> member_order{
"method_order", member_order_t::lexical};
option<bool> generate_packages{"generate_packages", false};
option<package_type_t> package_type{
"package_type", package_type_t::kNamespace};

View File

@@ -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<method_arguments>(
}
}
template <>
void get_option<member_order_t>(
const Node &node, clanguml::config::option<member_order_t> &option)
{
if (node[option.name]) {
const auto &val = node[option.name].as<std::string>();
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<package_type_t>(
const Node &node, clanguml::config::option<package_type_t> &option)
@@ -139,7 +155,6 @@ void get_option<std::map<std::string, clanguml::config::diagram_template>>(
YAML::Node included_node = YAML::LoadFile(include_path.string());
// diagram_config = parse_diagram_config(included_node);
option.set(
included_node.as<
std::map<std::string, clanguml::config::diagram_template>>());
@@ -419,6 +434,8 @@ template <> struct convert<class_diagram> {
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);

View File

@@ -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

View File

@@ -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