From 4423b14b628815c630d14c090f978737bb6706f3 Mon Sep 17 00:00:00 2001 From: Bartek Kryza Date: Sat, 4 Feb 2023 15:42:46 +0100 Subject: [PATCH] Added support for 'together' option in class diagrams with rendered namespaces --- .../plantuml/class_diagram_generator.cc | 124 ++++++++++++------ .../plantuml/class_diagram_generator.h | 11 +- src/common/generators/nested_element_stack.h | 74 +++++++++++ tests/t00054/.clang-uml | 24 ++++ tests/t00054/t00054.cc | 48 +++++++ tests/t00054/test_case.h | 59 +++++++++ tests/test_cases.cc | 1 + tests/test_cases.yaml | 5 +- 8 files changed, 301 insertions(+), 45 deletions(-) create mode 100644 src/common/generators/nested_element_stack.h create mode 100644 tests/t00054/.clang-uml create mode 100644 tests/t00054/t00054.cc create mode 100644 tests/t00054/test_case.h diff --git a/src/class_diagram/generators/plantuml/class_diagram_generator.cc b/src/class_diagram/generators/plantuml/class_diagram_generator.cc index f30800a4..6c3b0541 100644 --- a/src/class_diagram/generators/plantuml/class_diagram_generator.cc +++ b/src/class_diagram/generators/plantuml/class_diagram_generator.cc @@ -26,6 +26,7 @@ namespace clanguml::class_diagram::generators::plantuml { generator::generator(diagram_config &config, diagram_model &model) : common_generator{config, model} + , together_group_stack_{!config.generate_packages()} { } @@ -288,6 +289,25 @@ void generator::generate_member_notes(std::ostream &ostr, } } +void generator::generate_relationships(std::ostream &ostr) const +{ + for (const auto &p : m_model) { + if (auto *pkg = dynamic_cast(p.get()); pkg) { + generate_relationships(*pkg, ostr); + } + else if (auto *cls = dynamic_cast(p.get()); cls) { + if (m_model.should_include(*cls)) { + generate_relationships(*cls, ostr); + } + } + else if (auto *enm = dynamic_cast(p.get()); enm) { + if (m_model.should_include(*enm)) { + generate_relationships(*enm, ostr); + } + } + } +} + void generator::generate_relationships( const class_ &c, std::ostream &ostr) const { @@ -474,40 +494,65 @@ void generator::generate(const package &p, std::ostream &ostr) const if (dynamic_cast(subpackage.get()) != nullptr) { // TODO: add option - generate_empty_packages const auto &sp = dynamic_cast(*subpackage); - if (!sp.is_empty()) + if (!sp.is_empty()) { + together_group_stack_.enter(); + generate(sp, ostr); + + together_group_stack_.leave(); + } } - else if (dynamic_cast(subpackage.get()) != nullptr) { + else if (auto *cls = dynamic_cast(subpackage.get()); cls) { if (m_model.should_include(*subpackage)) { auto together_group = - m_config.get_together_group(subpackage->full_name(false)); + m_config.get_together_group(cls->full_name(false)); if (together_group) { - current_level_together_groups_[together_group.value()] - .push_back(subpackage.get()); + together_group_stack_.group_together( + together_group.value(), cls); } else { - generate_alias(dynamic_cast(*subpackage), ostr); - generate(dynamic_cast(*subpackage), ostr); + generate_alias(*cls, ostr); + generate(*cls, ostr); } } } - else if (dynamic_cast(subpackage.get()) != nullptr) { + else if (auto *enm = dynamic_cast(subpackage.get()); enm) { if (m_model.should_include(*subpackage)) { auto together_group = m_config.get_together_group(subpackage->full_name(false)); if (together_group) { - current_level_together_groups_[together_group.value()] - .push_back(subpackage.get()); + together_group_stack_.group_together( + together_group.value(), enm); } else { - generate_alias(dynamic_cast(*subpackage), ostr); - generate(dynamic_cast(*subpackage), ostr); + generate_alias(*enm, ostr); + generate(*enm, ostr); } } } } if (m_config.generate_packages()) { + // Now generate any diagram elements which are in together + // groups + for (const auto &[group_name, group_elements] : + together_group_stack_.get_current_groups()) { + ostr << "together {\n"; + + for (auto *e : group_elements) { + if (auto *cls = dynamic_cast(e); cls) { + generate_alias(*cls, ostr); + generate(*cls, ostr); + } + if (auto *enm = dynamic_cast(e); enm) { + generate_alias(*enm, ostr); + generate(*enm, ostr); + } + } + + ostr << "}\n"; + } + // Don't generate packages from namespaces filtered out by // using_namespace if (!uns.starts_with({p.full_name(false)})) { @@ -551,6 +596,21 @@ void generator::generate(std::ostream &ostr) const generate_plantuml_directives(ostr, m_config.puml().before); + generate_top_level_elements(ostr); + + generate_groups(ostr); + + generate_relationships(ostr); + + generate_config_layout_hints(ostr); + + generate_plantuml_directives(ostr, m_config.puml().after); + + ostr << "@enduml" << '\n'; +} + +void generator::generate_top_level_elements(std::ostream &ostr) const +{ for (const auto &p : m_model) { if (auto *pkg = dynamic_cast(p.get()); pkg) { if (!pkg->is_empty()) @@ -561,8 +621,8 @@ void generator::generate(std::ostream &ostr) const auto together_group = m_config.get_together_group(cls->full_name(false)); if (together_group) { - current_level_together_groups_[together_group.value()] - .push_back(cls); + together_group_stack_.group_together( + together_group.value(), cls); } else { generate_alias(*cls, ostr); @@ -575,8 +635,8 @@ void generator::generate(std::ostream &ostr) const auto together_group = m_config.get_together_group(enm->full_name(false)); if (together_group) { - current_level_together_groups_[together_group.value()] - .push_back(enm); + together_group_stack_.group_together( + together_group.value(), enm); } else { generate_alias(*enm, ostr); @@ -585,13 +645,14 @@ void generator::generate(std::ostream &ostr) const } } } +} - // Now generate any diagram elements which are in together groups +void generator::generate_groups(std::ostream &ostr) const +{ for (const auto &[group_name, group_elements] : - current_level_together_groups_) { - - ostr << "' together group for " << group_name << "\n"; + together_group_stack_.get_current_groups()) { ostr << "together {\n"; + for (auto *e : group_elements) { if (auto *cls = dynamic_cast(e); cls) { generate_alias(*cls, ostr); @@ -602,30 +663,9 @@ void generator::generate(std::ostream &ostr) const generate(*enm, ostr); } } + ostr << "}\n"; } - - for (const auto &p : m_model) { - if (auto *pkg = dynamic_cast(p.get()); pkg) { - generate_relationships(*pkg, ostr); - } - else if (auto *cls = dynamic_cast(p.get()); cls) { - if (m_model.should_include(*cls)) { - generate_relationships(*cls, ostr); - } - } - else if (auto *enm = dynamic_cast(p.get()); enm) { - if (m_model.should_include(*enm)) { - generate_relationships(*enm, ostr); - } - } - } - - generate_config_layout_hints(ostr); - - generate_plantuml_directives(ostr, m_config.puml().after); - - ostr << "@enduml" << '\n'; } } // namespace clanguml::class_diagram::generators::plantuml diff --git a/src/class_diagram/generators/plantuml/class_diagram_generator.h b/src/class_diagram/generators/plantuml/class_diagram_generator.h index f4539f26..063a286b 100644 --- a/src/class_diagram/generators/plantuml/class_diagram_generator.h +++ b/src/class_diagram/generators/plantuml/class_diagram_generator.h @@ -21,6 +21,7 @@ #include "class_diagram/model/diagram.h" #include "class_diagram/model/enum.h" #include "class_diagram/visitor/translation_unit_visitor.h" +#include "common/generators/nested_element_stack.h" #include "common/generators/plantuml/generator.h" #include "common/model/relationship.h" #include "config/config.h" @@ -66,6 +67,10 @@ public: void generate(const class_ &c, 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(const enum_ &e, std::ostream &ostr) const; @@ -79,13 +84,15 @@ public: void generate_member_notes(std::ostream &ostream, const class_element &member, const std::string &basicString) const; + void generate_groups(std::ostream &ostr) const; + void generate(std::ostream &ostr) const override; private: std::string render_name(std::string name) const; - mutable std::map> - current_level_together_groups_; + mutable common::generators::nested_element_stack + together_group_stack_; }; } // namespace plantuml diff --git a/src/common/generators/nested_element_stack.h b/src/common/generators/nested_element_stack.h new file mode 100644 index 00000000..e8005561 --- /dev/null +++ b/src/common/generators/nested_element_stack.h @@ -0,0 +1,74 @@ +/** + * src/common/generators/nested_element_stack.h + * + * Copyright (c) 2021-2023 Bartek Kryza + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include +#include + +namespace clanguml::common::generators { + +template class nested_element_stack { +public: + nested_element_stack(bool is_flat) + : is_flat_{is_flat} + , current_level_{0} + { + current_level_groups_.push_back({}); + } + + void enter() + { + if (!is_flat_) + current_level_++; + + current_level_groups_.push_back({}); + } + + void leave() + { + if (!is_flat_) + current_level_--; + + current_level_groups_.pop_back(); + } + + void group_together(const std::string &group_name, T *e) + { + current_level_groups_[current_level_][group_name].push_back(e); + } + + const std::map> &get_current_groups() + { + return current_level_groups_.at(current_level_); + } + + const std::vector &get_group(const std::string &group_name) + { + return get_current_groups().at(group_name); + } + +private: + bool is_flat_; + + uint32_t current_level_; + + std::vector>> current_level_groups_; +}; + +} // namespace clanguml::common::generators \ No newline at end of file diff --git a/tests/t00054/.clang-uml b/tests/t00054/.clang-uml new file mode 100644 index 00000000..3bd9b035 --- /dev/null +++ b/tests/t00054/.clang-uml @@ -0,0 +1,24 @@ +compilation_database_dir: .. +output_directory: puml +diagrams: + t00054_class: + type: class + glob: + - ../../tests/t00054/t00054.cc + include: + namespaces: + - clanguml::t00054 + using_namespace: + - clanguml::t00054 + generate_packages: true + layout: + a: + - together: [f] + "detail::c": + - together: [detail::e] + A: + - together: [B,G] + "detail2::detail3::D": + - together: [detail2::detail3::E] + "detail4::h": + - together: [detail4::i,detail4::j] \ No newline at end of file diff --git a/tests/t00054/t00054.cc b/tests/t00054/t00054.cc new file mode 100644 index 00000000..4f8080be --- /dev/null +++ b/tests/t00054/t00054.cc @@ -0,0 +1,48 @@ +namespace clanguml { +namespace t00054 { +struct a { +}; +struct b { +}; + +namespace detail { +struct c { +}; +struct d { +}; +struct e { +}; +} // namespace detail +struct f { +}; +struct g { +}; + +struct A { +}; +struct B { +}; + +namespace detail2 { +struct C { +}; +namespace detail3 { +struct D { +}; +struct E { +}; +} // namespace detail3 +struct F { +}; +} // namespace detail2 +struct G { +}; + +namespace detail4 { +enum class h { hhh }; +enum class i { iii }; +enum class j { jjj }; +} // namespace detail4 + +} +} \ No newline at end of file diff --git a/tests/t00054/test_case.h b/tests/t00054/test_case.h new file mode 100644 index 00000000..9fea467f --- /dev/null +++ b/tests/t00054/test_case.h @@ -0,0 +1,59 @@ +/** + * tests/t00054/test_case.h + * + * Copyright (c) 2021-2023 Bartek Kryza + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +TEST_CASE("t00054", "[test-case][class]") +{ + auto [config, db] = load_config("t00054"); + + auto diagram = config.diagrams["t00054_class"]; + + REQUIRE(diagram->name == "t00054_class"); + + auto model = generate_class_diagram(*db, diagram); + + REQUIRE(model->name() == "t00054_class"); + + auto puml = generate_class_puml(diagram, *model); + AliasMatcher _A(puml); + + REQUIRE_THAT(puml, StartsWith("@startuml")); + REQUIRE_THAT(puml, EndsWith("@enduml\n")); + + // Check if all classes exist + REQUIRE_THAT(puml, IsClass(_A("a"))); + REQUIRE_THAT(puml, IsClass(_A("b"))); + REQUIRE_THAT(puml, IsClass(_A("c"))); + REQUIRE_THAT(puml, IsClass(_A("d"))); + REQUIRE_THAT(puml, IsClass(_A("e"))); + REQUIRE_THAT(puml, IsClass(_A("f"))); + REQUIRE_THAT(puml, IsClass(_A("g"))); + + REQUIRE_THAT(puml, IsClass(_A("A"))); + REQUIRE_THAT(puml, IsClass(_A("B"))); + REQUIRE_THAT(puml, IsClass(_A("C"))); + REQUIRE_THAT(puml, IsClass(_A("D"))); + REQUIRE_THAT(puml, IsClass(_A("E"))); + REQUIRE_THAT(puml, IsClass(_A("F"))); + REQUIRE_THAT(puml, IsClass(_A("G"))); + + REQUIRE_THAT(puml, IsEnum(_A("i"))); + REQUIRE_THAT(puml, IsEnum(_A("h"))); + REQUIRE_THAT(puml, IsEnum(_A("j"))); + + save_puml(config.output_directory() + "/" + diagram->name + ".puml", puml); +} \ No newline at end of file diff --git a/tests/test_cases.cc b/tests/test_cases.cc index ae215fa2..48770f20 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -247,6 +247,7 @@ using namespace clanguml::test::matchers; #include "t00051/test_case.h" #include "t00052/test_case.h" #include "t00053/test_case.h" +#include "t00054/test_case.h" /// /// Sequence diagram tests diff --git a/tests/test_cases.yaml b/tests/test_cases.yaml index e97756bd..9ef26121 100644 --- a/tests/test_cases.yaml +++ b/tests/test_cases.yaml @@ -154,7 +154,10 @@ test_cases: title: Test case for template methods rendering description: - name: t00053 - title: Test case for `together` layout hint + title: Test case for `together` layout hint in class diagram + description: + - name: t00054 + title: Test case for `together` layout hint in class diagram with rendered namespaces description: Sequence diagrams: - name: t20001