From c51ae5b6eeba731b56090b54f358a5ec41f07174 Mon Sep 17 00:00:00 2001 From: Bartek Kryza Date: Mon, 18 Dec 2023 21:55:18 +0100 Subject: [PATCH] Added support for C++20 module based packages in class diagrams (#101) --- .../json/class_diagram_generator.cc | 11 ++- .../plantuml/class_diagram_generator.cc | 6 +- .../visitor/translation_unit_visitor.cc | 27 +++++++ src/common/model/diagram_filter.cc | 5 +- src/common/model/path.h | 7 +- src/config/config.cc | 26 +++++++ src/config/config.h | 25 ++++++- src/config/schema.h | 4 ++ src/config/yaml_decoders.cc | 4 ++ src/config/yaml_emitters.cc | 1 + tests/CMakeLists.txt | 2 +- tests/t00065/test_case.h | 2 +- tests/t00071/.clang-uml | 12 ++++ tests/t00071/src/lib1.cppm | 13 ++++ tests/t00071/src/lib1mod1.cppm | 5 ++ tests/t00071/src/lib1mod2.cppm | 5 ++ tests/t00071/src/lib2.cppm | 13 ++++ tests/t00071/src/t00071_mod.cppm | 11 +++ tests/t00071/t00071.cc | 15 ++++ tests/t00071/test_case.h | 70 +++++++++++++++++++ tests/test_cases.cc | 1 + tests/test_cases.yaml | 6 ++ tests/test_config.cc | 25 +++++++ tests/test_config_data/using_module.yml | 12 ++++ 24 files changed, 296 insertions(+), 12 deletions(-) create mode 100644 tests/t00071/.clang-uml create mode 100644 tests/t00071/src/lib1.cppm create mode 100644 tests/t00071/src/lib1mod1.cppm create mode 100644 tests/t00071/src/lib1mod2.cppm create mode 100644 tests/t00071/src/lib2.cppm create mode 100644 tests/t00071/src/t00071_mod.cppm create mode 100644 tests/t00071/t00071.cc create mode 100644 tests/t00071/test_case.h create mode 100644 tests/test_config_data/using_module.yml diff --git a/src/class_diagram/generators/json/class_diagram_generator.cc b/src/class_diagram/generators/json/class_diagram_generator.cc index 21bbe020..66966d5e 100644 --- a/src/class_diagram/generators/json/class_diagram_generator.cc +++ b/src/class_diagram/generators/json/class_diagram_generator.cc @@ -170,10 +170,17 @@ void generator::generate(const package &p, nlohmann::json &parent) const if (!uns.starts_with({p.full_name(false)})) { LOG_DBG("Generating package {}", p.name()); - if (config().package_type() == config::package_type_t::kDirectory) + switch (config().package_type()) { + case config::package_type_t::kDirectory: package_object["type"] = "directory"; - else + break; + case config::package_type_t::kModule: + package_object["type"] = "module"; + break; + case config::package_type_t::kNamespace: package_object["type"] = "namespace"; + break; + } package_object["name"] = p.name(); package_object["display_name"] = p.full_name(false); diff --git a/src/class_diagram/generators/plantuml/class_diagram_generator.cc b/src/class_diagram/generators/plantuml/class_diagram_generator.cc index 35c299e7..231ac18b 100644 --- a/src/class_diagram/generators/plantuml/class_diagram_generator.cc +++ b/src/class_diagram/generators/plantuml/class_diagram_generator.cc @@ -67,7 +67,7 @@ void generator::generate_alias(const class_ &c, std::ostream &ostr) const class_type = "abstract"; std::string full_name; - if (config().generate_packages()) + if (config().generate_fully_qualified_name()) full_name = c.full_name_no_ns(); else full_name = c.full_name(); @@ -89,7 +89,7 @@ void generator::generate_alias(const enum_ &e, std::ostream &ostr) const { print_debug(e, ostr); - if (config().generate_packages()) + if (config().generate_fully_qualified_name()) ostr << "enum" << " \"" << e.name(); else @@ -106,7 +106,7 @@ void generator::generate_alias(const concept_ &c, std::ostream &ostr) const { print_debug(c, ostr); - if (config().generate_packages()) + if (config().generate_fully_qualified_name()) ostr << "class" << " \"" << c.name(); else diff --git a/src/class_diagram/visitor/translation_unit_visitor.cc b/src/class_diagram/visitor/translation_unit_visitor.cc index c1fe98df..489b48b3 100644 --- a/src/class_diagram/visitor/translation_unit_visitor.cc +++ b/src/class_diagram/visitor/translation_unit_visitor.cc @@ -2150,6 +2150,15 @@ void translation_unit_visitor::add_class(std::unique_ptr &&c) diagram().add(p, std::move(c)); } + else if ((config().generate_packages() && + config().package_type() == config::package_type_t::kModule)) { + + const auto module_path = config().make_module_relative(c->module()); + + common::model::path p{module_path, common::model::path_type::kModule}; + + diagram().add(p, std::move(c)); + } else { diagram().add(c->path(), std::move(c)); } @@ -2169,6 +2178,15 @@ void translation_unit_visitor::add_enum(std::unique_ptr &&e) diagram().add(p, std::move(e)); } + else if ((config().generate_packages() && + config().package_type() == config::package_type_t::kModule)) { + + const auto module_path = config().make_module_relative(e->module()); + + common::model::path p{module_path, common::model::path_type::kModule}; + + diagram().add(p, std::move(e)); + } else { diagram().add(e->path(), std::move(e)); } @@ -2188,6 +2206,15 @@ void translation_unit_visitor::add_concept(std::unique_ptr &&c) diagram().add(p, std::move(c)); } + else if ((config().generate_packages() && + config().package_type() == config::package_type_t::kModule)) { + + const auto module_path = config().make_module_relative(c->module()); + + common::model::path p{module_path, common::model::path_type::kModule}; + + diagram().add(p, std::move(c)); + } else { diagram().add(c->path(), std::move(c)); } diff --git a/src/common/model/diagram_filter.cc b/src/common/model/diagram_filter.cc index 493b07db..bd501051 100644 --- a/src/common/model/diagram_filter.cc +++ b/src/common/model/diagram_filter.cc @@ -292,7 +292,8 @@ modules_filter::modules_filter( { } -tvl::value_t modules_filter::match(const diagram &d, const element &e) const +tvl::value_t modules_filter::match( + const diagram & /*d*/, const element &e) const { if (modules_.empty()) return {}; @@ -300,7 +301,7 @@ tvl::value_t modules_filter::match(const diagram &d, const element &e) const if (!e.module().has_value()) return {false}; - const auto module_toks = util::split(e.module().value(), "."); + const auto module_toks = util::split(e.module().value(), "."); // NOLINT auto result = tvl::any_of(modules_.begin(), modules_.end(), [&e, &module_toks](const auto &modit) { diff --git a/src/common/model/path.h b/src/common/model/path.h index d9bd4bc8..aa8e9295 100644 --- a/src/common/model/path.h +++ b/src/common/model/path.h @@ -32,8 +32,9 @@ namespace clanguml::common::model { * a nested set of namespaces or nested set of directories. */ enum class path_type { - kNamespace, /*!< Namespace path */ - kFilesystem /*!< Filesystem path */ + kNamespace, /*!< Namespace path */ + kFilesystem, /*!< Filesystem path */ + kModule /*!< Module path */ }; /** @@ -54,6 +55,8 @@ class path { switch (path_type_) { case path_type::kNamespace: return "::"; + case path_type::kModule: + return "."; case path_type::kFilesystem: #ifdef _WIN32 return "\\"; diff --git a/src/config/config.cc b/src/config/config.cc index ecfd9722..11207f86 100644 --- a/src/config/config.cc +++ b/src/config/config.cc @@ -187,6 +187,7 @@ void inheritable_diagram_options::inherit( { glob.override(parent.glob); using_namespace.override(parent.using_namespace); + using_module.override(parent.using_module); include_relations_also_as_members.override( parent.include_relations_also_as_members); include.override(parent.include); @@ -229,6 +230,12 @@ std::string inheritable_diagram_options::simplify_template_type( return full_name; } +bool inheritable_diagram_options::generate_fully_qualified_name() const +{ + return generate_packages() && + (package_type() == package_type_t::kNamespace); +} + std::vector diagram::get_translation_units() const { std::vector translation_units{}; @@ -264,6 +271,25 @@ std::filesystem::path diagram::make_path_relative( return relative(p, root_directory()).lexically_normal().string(); } +std::vector diagram::make_module_relative( + const std::optional &maybe_module) const +{ + if (!maybe_module) + return {}; + + auto module_path = util::split(maybe_module.value(), "."); + + if (using_module.has_value) { + auto using_module_path = util::split(using_module(), "."); + + if (util::starts_with(module_path, using_module_path)) { + util::remove_prefix(module_path, using_module_path); + } + } + + return module_path; +} + std::optional diagram::get_together_group( const std::string &full_name) const { diff --git a/src/config/config.h b/src/config/config.h index 7b061b78..8dc54064 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -87,7 +87,8 @@ std::string to_string(callee_type mt); /*! How packages in diagrams should be generated */ enum class package_type_t { kNamespace, /*!< From namespaces */ - kDirectory /*!< From directories */ + kDirectory, /*!< From directories */ + kModule /*!< From modules */ }; std::string to_string(package_type_t mt); @@ -469,6 +470,18 @@ struct inheritable_diagram_options { std::string simplify_template_type(std::string full_name) const; + /** + * @brief Whether the diagram element should be fully qualified in diagram + * + * This method determines whether an elements' name should include + * fully qualified namespace name (however relative to using_namespace), or + * whether it should just contain it's name. This depends on whether the + * diagram has packages and if they are based on namespaces or sth else. + * + * @return True, if element should include it's namespace + */ + bool generate_fully_qualified_name() const; + /** * @brief Get reference to `relative_to` diagram config option * @@ -483,6 +496,7 @@ struct inheritable_diagram_options { option> glob{"glob"}; option using_namespace{"using_namespace"}; + option using_module{"using_module"}; option include_relations_also_as_members{ "include_relations_also_as_members", true}; option include{"include"}; @@ -566,6 +580,15 @@ struct diagram : public inheritable_diagram_options { std::filesystem::path make_path_relative( const std::filesystem::path &p) const; + /** + * @brief Make module path relative to `using_module` configuration option + * + * @param p Input path + * @return Relative path + */ + std::vector make_module_relative( + const std::optional &maybe_module) const; + /** * @brief Returns absolute path of the `relative_to` option * diff --git a/src/config/schema.h b/src/config/schema.h index 21827d67..bf8e2ea0 100644 --- a/src/config/schema.h +++ b/src/config/schema.h @@ -58,6 +58,7 @@ types: package_type_t: !variant - namespace - directory + - module member_order_t: !variant - lexical - as_is @@ -161,6 +162,7 @@ types: cmd: !optional string relative_to: !optional string using_namespace: !optional [string, [string]] + using_module: !optional string generate_metadata: !optional bool title: !optional string # @@ -239,6 +241,7 @@ types: cmd: !optional string relative_to: !optional string using_namespace: !optional [string, [string]] + using_module: !optional string generate_metadata: !optional bool title: !optional string # @@ -318,6 +321,7 @@ root: cmd: !optional string relative_to: !optional string using_namespace: !optional [string, [string]] + using_module: !optional string generate_metadata: !optional bool # # Inheritable custom options diff --git a/src/config/yaml_decoders.cc b/src/config/yaml_decoders.cc index c24686ea..82c7c0ef 100644 --- a/src/config/yaml_decoders.cc +++ b/src/config/yaml_decoders.cc @@ -133,6 +133,8 @@ void get_option( option.set(package_type_t::kNamespace); else if (val == "directory") option.set(package_type_t::kDirectory); + else if (val == "module") + option.set(package_type_t::kModule); else throw std::runtime_error( "Invalid generate_method_arguments value: " + val); @@ -573,6 +575,7 @@ template bool decode_diagram(const Node &node, T &rhs) // Decode options common for all diagrams get_option(node, rhs.glob); get_option(node, rhs.using_namespace); + get_option(node, rhs.using_module); get_option(node, rhs.include); get_option(node, rhs.exclude); get_option(node, rhs.puml); @@ -787,6 +790,7 @@ template <> struct convert { { get_option(node, rhs.glob); get_option(node, rhs.using_namespace); + get_option(node, rhs.using_module); get_option(node, rhs.output_directory); get_option(node, rhs.compilation_database_dir); get_option(node, rhs.add_compile_flags); diff --git a/src/config/yaml_emitters.cc b/src/config/yaml_emitters.cc index 2bdad164..c850287b 100644 --- a/src/config/yaml_emitters.cc +++ b/src/config/yaml_emitters.cc @@ -310,6 +310,7 @@ YAML::Emitter &operator<<( out << c.puml; out << c.relative_to; out << c.using_namespace; + out << c.using_module; out << c.generate_metadata; if (const auto *cd = dynamic_cast(&c); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 982ca588..300bde57 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,7 +7,7 @@ file(GLOB_RECURSE TEST_CONFIG_YMLS test_config_data/*.yml test_compilation_database_data/*.json) set(TEST_CASES_REQUIRING_CXX20 t00056 t00058 t00059 t00065 t00069) -set(TEST_CASES_REQUIRING_CXX20_MODULES t00070) +set(TEST_CASES_REQUIRING_CXX20_MODULES t00070 t00071) if(ENABLE_CXX_MODULES_TEST_CASES) foreach(CXX20_MOD_TC ${TEST_CASES_REQUIRING_CXX20_MODULES}) diff --git a/tests/t00065/test_case.h b/tests/t00065/test_case.h index 0a1c0fdf..539d7944 100644 --- a/tests/t00065/test_case.h +++ b/tests/t00065/test_case.h @@ -38,7 +38,7 @@ TEST_CASE("t00065", "[test-case][class]") // Check if all classes exist REQUIRE_THAT(src, IsClass(_A("R"))); REQUIRE_THAT(src, IsClass(_A("A"))); - REQUIRE_THAT(src, IsClass(_A("AImpl"))); + REQUIRE_THAT(src, IsClass(_A("detail::AImpl"))); REQUIRE_THAT(src, IsEnum(_A("XYZ"))); REQUIRE_THAT(src, IsEnum(_A("ABC"))); diff --git a/tests/t00071/.clang-uml b/tests/t00071/.clang-uml new file mode 100644 index 00000000..1e063e26 --- /dev/null +++ b/tests/t00071/.clang-uml @@ -0,0 +1,12 @@ +diagrams: + t00071_class: + type: class + glob: + - t00071.cc + include: + namespaces: + - clanguml::t00071 + generate_packages: true + package_type: module + using_namespace: clanguml::t00071 + using_module: t00071 \ No newline at end of file diff --git a/tests/t00071/src/lib1.cppm b/tests/t00071/src/lib1.cppm new file mode 100644 index 00000000..62c833db --- /dev/null +++ b/tests/t00071/src/lib1.cppm @@ -0,0 +1,13 @@ +export module t00071.app.lib1; + +export namespace clanguml::t00071 { +class B { }; + +template class BB { + T t; +}; + +namespace detail { +enum class BBB { bbb1, bbb2 }; +} // namespace detail +} \ No newline at end of file diff --git a/tests/t00071/src/lib1mod1.cppm b/tests/t00071/src/lib1mod1.cppm new file mode 100644 index 00000000..1734e8e7 --- /dev/null +++ b/tests/t00071/src/lib1mod1.cppm @@ -0,0 +1,5 @@ +export module t00071.app.lib1.mod1; + +export namespace clanguml::t00071 { +class D { }; +} \ No newline at end of file diff --git a/tests/t00071/src/lib1mod2.cppm b/tests/t00071/src/lib1mod2.cppm new file mode 100644 index 00000000..7bb97560 --- /dev/null +++ b/tests/t00071/src/lib1mod2.cppm @@ -0,0 +1,5 @@ +export module t00071.app.lib1.mod2; + +export namespace clanguml::t00071 { +class E { }; +} \ No newline at end of file diff --git a/tests/t00071/src/lib2.cppm b/tests/t00071/src/lib2.cppm new file mode 100644 index 00000000..591f15b5 --- /dev/null +++ b/tests/t00071/src/lib2.cppm @@ -0,0 +1,13 @@ +export module t00071.app.lib2; + +export namespace clanguml::t00071 { +class C { }; + +template class CC { + T t; +}; + +namespace detail { +enum class CCC { ccc1, ccc2 }; +} +} \ No newline at end of file diff --git a/tests/t00071/src/t00071_mod.cppm b/tests/t00071/src/t00071_mod.cppm new file mode 100644 index 00000000..0df8e4bb --- /dev/null +++ b/tests/t00071/src/t00071_mod.cppm @@ -0,0 +1,11 @@ +export module t00071.app; +export import t00071.app.lib1; +export import t00071.app.lib2; + +export namespace clanguml::t00071 { +class A { + int get() { return a; } + + int a; +}; +} \ No newline at end of file diff --git a/tests/t00071/t00071.cc b/tests/t00071/t00071.cc new file mode 100644 index 00000000..cfc6f65f --- /dev/null +++ b/tests/t00071/t00071.cc @@ -0,0 +1,15 @@ +import t00071.app; +import t00071.app.lib1; +import t00071.app.lib1.mod1; +import t00071.app.lib1.mod2; +import t00071.app.lib2; + +namespace clanguml { +namespace t00071 { +class R { + A *a; + B *b; + C *c; +}; +} +} \ No newline at end of file diff --git a/tests/t00071/test_case.h b/tests/t00071/test_case.h new file mode 100644 index 00000000..7d946330 --- /dev/null +++ b/tests/t00071/test_case.h @@ -0,0 +1,70 @@ +/** + * tests/t00071/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("t00071", "[test-case][class]") +{ + auto [config, db] = load_config("t00071"); + + auto diagram = config.diagrams["t00071_class"]; + + REQUIRE(diagram->name == "t00071_class"); + + auto model = generate_class_diagram(*db, diagram); + + REQUIRE(model->name() == "t00071_class"); + + { + auto src = generate_class_puml(diagram, *model); + AliasMatcher _A(src); + + REQUIRE_THAT(src, StartsWith("@startuml")); + REQUIRE_THAT(src, EndsWith("@enduml\n")); + + REQUIRE_THAT(src, IsClass(_A("A"))); + REQUIRE_THAT(src, IsClass(_A("R"))); + + REQUIRE_THAT(src, IsEnum(_A("detail::BBB"))); + REQUIRE_THAT(src, IsEnum(_A("detail::CCC"))); + + save_puml(config.output_directory(), diagram->name + ".puml", src); + } + + { + auto j = generate_class_json(diagram, *model); + + using namespace json; + + save_json(config.output_directory(), diagram->name + ".json", j); + } + + { + auto src = generate_class_mermaid(diagram, *model); + + mermaid::AliasMatcher _A(src); + using mermaid::IsClass; + using mermaid::IsEnum; + + REQUIRE_THAT(src, IsClass(_A("A"))); + REQUIRE_THAT(src, IsClass(_A("R"))); + + REQUIRE_THAT(src, IsEnum(_A("detail::BBB"))); + REQUIRE_THAT(src, IsEnum(_A("detail::CCC"))); + + save_mermaid(config.output_directory(), diagram->name + ".mmd", src); + } +} \ No newline at end of file diff --git a/tests/test_cases.cc b/tests/test_cases.cc index ebf1ddb9..8cefa23e 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -409,6 +409,7 @@ using namespace clanguml::test::matchers; #endif #if defined(ENABLE_CXX_MODULES_TEST_CASES) #include "t00070/test_case.h" +#include "t00071/test_case.h" #endif /// diff --git a/tests/test_cases.yaml b/tests/test_cases.yaml index c212aa5a..6ec49260 100644 --- a/tests/test_cases.yaml +++ b/tests/test_cases.yaml @@ -204,6 +204,12 @@ test_cases: - name: t00069 title: Coroutine methods in class diagrams description: + - name: t00070 + title: Diagram filter based on C++20 modules + description: + - name: t00071 + title: Class diagram with C++20 modules generated as packages + description: Sequence diagrams: - name: t20001 title: Basic sequence diagram test case diff --git a/tests/test_config.cc b/tests/test_config.cc index 53d052c5..ee622e3a 100644 --- a/tests/test_config.cc +++ b/tests/test_config.cc @@ -377,6 +377,31 @@ TEST_CASE("Test config relative paths handling", "[unit-test]") "{}/test_config_data", std::filesystem::current_path().string())); } +TEST_CASE("Test using_module relative to", "[unit-test]") +{ + auto cfg = clanguml::config::load("./test_config_data/using_module.yml"); + + CHECK(cfg.diagrams.size() == 2); + auto &def = *cfg.diagrams["class1"]; + CHECK(def.make_module_relative(std::make_optional( + "mod1.mod2.mod3")) == std::vector{std::string{"mod3"}}); + CHECK(def.make_module_relative(std::make_optional( + "mod1.mod2")) == std::vector{}); + CHECK(def.make_module_relative( + std::make_optional("modA.modB.modC")) == + std::vector{ + std::string{"modA"}, std::string{"modB"}, std::string{"modC"}}); + + def = *cfg.diagrams["class2"]; + CHECK(def.make_module_relative( + std::make_optional("mod1.mod2.mod3")) == + std::vector{std::string{"mod2"}, std::string{"mod3"}}); + CHECK(def.make_module_relative( + std::make_optional("modA.modB.modC")) == + std::vector{ + std::string{"modA"}, std::string{"modB"}, std::string{"modC"}}); +} + TEST_CASE("Test config full clang uml dump", "[unit-test]") { auto cfg = diff --git a/tests/test_config_data/using_module.yml b/tests/test_config_data/using_module.yml new file mode 100644 index 00000000..490a65b9 --- /dev/null +++ b/tests/test_config_data/using_module.yml @@ -0,0 +1,12 @@ +diagrams: + class1: + type: class + glob: + - test.cc + using_module: mod1.mod2 + class2: + type: class + relative_to: . + glob: + - test.cc + using_module: mod1