From a8d646d1bc9263decf9d6b3d672b67aabd6562e0 Mon Sep 17 00:00:00 2001 From: Bartek Kryza Date: Thu, 21 Dec 2023 23:39:59 +0100 Subject: [PATCH] Added package diagram generation from C++20 modules (#101) --- src/common/model/element.cc | 5 +- src/common/model/element.h | 4 +- src/common/model/package.cc | 4 +- src/common/model/package.h | 3 +- src/common/model/path.h | 2 + src/package_diagram/model/diagram.h | 63 +++++++++++++++++ .../visitor/translation_unit_visitor.cc | 66 ++++++++++++++++- tests/CMakeLists.txt | 2 +- tests/t30012/.clang-uml | 10 +++ tests/t30012/src/lib1.cppm | 13 ++++ tests/t30012/src/lib1mod1.cppm | 5 ++ tests/t30012/src/lib1mod2.cppm | 5 ++ tests/t30012/src/lib2.cppm | 13 ++++ tests/t30012/src/t30012_mod.cppm | 11 +++ tests/t30012/t30012.cc | 15 ++++ tests/t30012/test_case.h | 70 +++++++++++++++++++ tests/test_cases.cc | 4 +- util/templates/test_cases/test_case.h | 2 +- 18 files changed, 285 insertions(+), 12 deletions(-) create mode 100644 tests/t30012/.clang-uml create mode 100644 tests/t30012/src/lib1.cppm create mode 100644 tests/t30012/src/lib1mod1.cppm create mode 100644 tests/t30012/src/lib1mod2.cppm create mode 100644 tests/t30012/src/lib2.cppm create mode 100644 tests/t30012/src/t30012_mod.cppm create mode 100644 tests/t30012/t30012.cc create mode 100644 tests/t30012/test_case.h diff --git a/src/common/model/element.cc b/src/common/model/element.cc index 038e75bb..ceb90e05 100644 --- a/src/common/model/element.cc +++ b/src/common/model/element.cc @@ -25,8 +25,9 @@ namespace clanguml::common::model { -element::element(namespace_ using_namespace) - : using_namespace_{std::move(using_namespace)} +element::element(namespace_ using_namespace, path_type pt) + : ns_{pt} + , using_namespace_{std::move(using_namespace)} { } diff --git a/src/common/model/element.h b/src/common/model/element.h index 0707c246..2d94d393 100644 --- a/src/common/model/element.h +++ b/src/common/model/element.h @@ -37,7 +37,9 @@ namespace clanguml::common::model { */ class element : public diagram_element { public: - element(namespace_ using_namespace); + element(namespace_ using_namespace, path_type pt = path_type::kNamespace); + + element(path_type pt); ~element() override = default; diff --git a/src/common/model/package.cc b/src/common/model/package.cc index 74035dd7..6cdc2b40 100644 --- a/src/common/model/package.cc +++ b/src/common/model/package.cc @@ -21,8 +21,8 @@ #include namespace clanguml::common::model { -package::package(const common::model::namespace_ &using_namespace) - : element{using_namespace} +package::package(const common::model::namespace_ &using_namespace, path_type pt) + : element{using_namespace, pt} { } diff --git a/src/common/model/package.h b/src/common/model/package.h index 425d6a87..3ed6f46c 100644 --- a/src/common/model/package.h +++ b/src/common/model/package.h @@ -41,7 +41,8 @@ class package : public element, public stylable_element, public nested_trait { public: - package(const common::model::path &using_namespace); + package(const common::model::path &using_namespace, + path_type pt = path_type::kNamespace); package(const package &) = delete; package(package &&) = default; diff --git a/src/common/model/path.h b/src/common/model/path.h index aa8e9295..3cb030eb 100644 --- a/src/common/model/path.h +++ b/src/common/model/path.h @@ -386,6 +386,8 @@ public: */ path_type type() const { return path_type_; } + const container_type &tokens() const { return path_; } + private: path_type path_type_; container_type path_; diff --git a/src/package_diagram/model/diagram.h b/src/package_diagram/model/diagram.h index 459e71ea..f995d86b 100644 --- a/src/package_diagram/model/diagram.h +++ b/src/package_diagram/model/diagram.h @@ -135,6 +135,9 @@ public: if (parent_path.type() == common::model::path_type::kNamespace) { return add_with_namespace_path(std::move(e)); } + else if (parent_path.type() == common::model::path_type::kModule) { + return add_with_module_path(parent_path, std::move(e)); + } return add_with_filesystem_path(parent_path, std::move(e)); } @@ -155,6 +158,17 @@ public: inja::json context() const override; private: + /** + * @brief Add element using module as diagram path + * + * @tparam ElementT Element type + * @param e Element to add + * @return True, if the element was added + */ + template + bool add_with_module_path( + const common::model::path &parent_path, std::unique_ptr &&e); + /** * @brief Add element using namespace as diagram path * @@ -237,6 +251,55 @@ bool diagram::add_with_namespace_path(std::unique_ptr &&p) return res; } +template +bool diagram::add_with_module_path( + const common::model::path &parent_path, std::unique_ptr &&p) +{ + LOG_DBG("Adding package: {}, {}, {}, [{}]", p->name(), p->full_name(false), + parent_path.to_string(), p->id()); + + // Make sure all parent modules are already packages in the + // model + std::string module_path = p->using_namespace().to_string(); + for (auto it = parent_path.begin(); it != parent_path.end(); it++) { + auto pkg = std::make_unique( + p->using_namespace(), common::model::path_type::kModule); + pkg->set_name(*it); + + auto ns = common::model::path( + parent_path.begin(), it, common::model::path_type::kModule); + pkg->set_module(module_path); + pkg->set_namespace(ns); + + std::string package_id_path; + if (module_path.empty()) + package_id_path = pkg->name(); + else + package_id_path = module_path + "." + pkg->name(); + + pkg->set_id(common::to_id(package_id_path)); + + auto p_ref = std::ref(*pkg); + + auto res = add_element(ns, std::move(pkg)); + if (res) + element_view::add(p_ref); + + if (module_path.empty()) + module_path = *it; + else + module_path += fmt::format(".{}", *it); + } + + auto p_ref = std::ref(*p); + + auto res = add_element(parent_path, std::move(p)); + if (res) + element_view::add(p_ref); + + return res; +} + template bool diagram::add_with_filesystem_path( const common::model::path &parent_path, std::unique_ptr &&p) diff --git a/src/package_diagram/visitor/translation_unit_visitor.cc b/src/package_diagram/visitor/translation_unit_visitor.cc index edabb3d8..e2fd4f7a 100644 --- a/src/package_diagram/visitor/translation_unit_visitor.cc +++ b/src/package_diagram/visitor/translation_unit_visitor.cc @@ -21,6 +21,8 @@ #include "common/clang_utils.h" #include "common/model/namespace.h" +#include "clang/Basic/Module.h" + #include #include @@ -45,7 +47,7 @@ bool translation_unit_visitor::VisitNamespaceDecl(clang::NamespaceDecl *ns) { assert(ns != nullptr); - if (config().package_type() == config::package_type_t::kDirectory) + if (config().package_type() != config::package_type_t::kNamespace) return true; if (ns->isAnonymousNamespace() || ns->isInline()) @@ -237,6 +239,43 @@ void translation_unit_visitor::add_relationships( if (diagram().should_include(*pkg)) diagram().add(parent_path, std::move(pkg)); } + else if (config().package_type() == config::package_type_t::kModule) { + const auto *module = cls->getOwningModule(); + + if (module == nullptr) { + return; + } + + std::string module_path_str = module->Name; + if (module->isPrivateModule()) + module_path_str = module->getTopLevelModule()->Name; + + common::model::path module_path{ + module_path_str, common::model::path_type::kModule}; + module_path.pop_back(); + + auto relative_module = + config().make_module_relative(std::optional{module_path_str}); + + common::model::path parent_path{ + relative_module, common::model::path_type::kModule}; + auto pkg_name = parent_path.name(); + parent_path.pop_back(); + + auto pkg = std::make_unique( + config().using_module(), common::model::path_type::kModule); + + pkg->set_name(pkg_name); + pkg->set_id(get_package_id(cls)); + // This is for diagram filters + pkg->set_module(module_path.to_string()); + // This is for rendering nested package structure + pkg->set_namespace(parent_path); + set_source_location(*cls, *pkg); + + if (diagram().should_include(*pkg)) + diagram().add(parent_path, std::move(pkg)); + } auto current_package_id = get_package_id(cls); @@ -284,6 +323,18 @@ common::model::diagram_element::id_t translation_unit_visitor::get_package_id( return {}; } + else if (config().package_type() == config::package_type_t::kModule) { + const auto *module = cls->getOwningModule(); + if (module != nullptr) { + std::string module_path = module->Name; + if (module->isPrivateModule()) { + module_path = module->getTopLevelModule()->Name; + } + return common::to_id(module_path); + } + + return {}; + } auto file = source_manager().getFilename(cls->getSourceRange().getBegin()).str(); @@ -578,6 +629,15 @@ bool translation_unit_visitor::find_relationships(const clang::QualType &type, } } } + else if (config().package_type() == + config::package_type_t::kModule) { + const auto *module = cxxrecord_decl->getOwningModule(); + if (module != nullptr) { + const auto target_id = get_package_id(cxxrecord_decl); + relationships.emplace_back(target_id, relationship_hint); + result = true; + } + } else { if (diagram().should_include( namespace_{common::get_qualified_name( @@ -591,8 +651,8 @@ bool translation_unit_visitor::find_relationships(const clang::QualType &type, } else if (const auto *record_decl = type->getAsRecordDecl(); record_decl != nullptr) { - // This is only possible for plain C translation unit, so we don't - // need to consider namespaces here + // This is only possible for plain C translation unit, so we + // don't need to consider namespaces or modules here if (config().package_type() == config::package_type_t::kDirectory) { if (diagram().should_include( namespace_{common::get_qualified_name(*record_decl)})) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 300bde57..85e627ad 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 t00071) +set(TEST_CASES_REQUIRING_CXX20_MODULES t00070 t00071 t30012) if(ENABLE_CXX_MODULES_TEST_CASES) foreach(CXX20_MOD_TC ${TEST_CASES_REQUIRING_CXX20_MODULES}) diff --git a/tests/t30012/.clang-uml b/tests/t30012/.clang-uml new file mode 100644 index 00000000..7335486c --- /dev/null +++ b/tests/t30012/.clang-uml @@ -0,0 +1,10 @@ +diagrams: + t30012_package: + type: package + glob: + - t30012.cc + package_type: module + include: + modules: + - t30012 + using_module: t30012 \ No newline at end of file diff --git a/tests/t30012/src/lib1.cppm b/tests/t30012/src/lib1.cppm new file mode 100644 index 00000000..6835aa09 --- /dev/null +++ b/tests/t30012/src/lib1.cppm @@ -0,0 +1,13 @@ +export module t30012.app.lib1; + +export namespace clanguml::t30012 { +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/t30012/src/lib1mod1.cppm b/tests/t30012/src/lib1mod1.cppm new file mode 100644 index 00000000..c3601405 --- /dev/null +++ b/tests/t30012/src/lib1mod1.cppm @@ -0,0 +1,5 @@ +export module t30012.app.lib1.mod1; + +export namespace clanguml::t30012 { +class D { }; +} \ No newline at end of file diff --git a/tests/t30012/src/lib1mod2.cppm b/tests/t30012/src/lib1mod2.cppm new file mode 100644 index 00000000..d393ea64 --- /dev/null +++ b/tests/t30012/src/lib1mod2.cppm @@ -0,0 +1,5 @@ +export module t30012.app.lib1.mod2; + +export namespace clanguml::t30012 { +class E { }; +} \ No newline at end of file diff --git a/tests/t30012/src/lib2.cppm b/tests/t30012/src/lib2.cppm new file mode 100644 index 00000000..99e1c94b --- /dev/null +++ b/tests/t30012/src/lib2.cppm @@ -0,0 +1,13 @@ +export module t30012.app.lib2; + +export namespace clanguml::t30012 { +class C { }; + +template class CC { + T t; +}; + +namespace detail { +enum class CCC { ccc1, ccc2 }; +} +} \ No newline at end of file diff --git a/tests/t30012/src/t30012_mod.cppm b/tests/t30012/src/t30012_mod.cppm new file mode 100644 index 00000000..9bcbd77a --- /dev/null +++ b/tests/t30012/src/t30012_mod.cppm @@ -0,0 +1,11 @@ +export module t30012.app; +export import t30012.app.lib1; +export import t30012.app.lib2; + +export namespace clanguml::t30012 { +class A { + int get() { return a; } + + int a; +}; +} \ No newline at end of file diff --git a/tests/t30012/t30012.cc b/tests/t30012/t30012.cc new file mode 100644 index 00000000..89f09ca1 --- /dev/null +++ b/tests/t30012/t30012.cc @@ -0,0 +1,15 @@ +import t30012.app; +import t30012.app.lib1; +import t30012.app.lib1.mod1; +import t30012.app.lib1.mod2; +import t30012.app.lib2; + +namespace clanguml { +namespace t30012 { +class R { + A *a; + B *b; + C *c; +}; +} +} \ No newline at end of file diff --git a/tests/t30012/test_case.h b/tests/t30012/test_case.h new file mode 100644 index 00000000..de394bda --- /dev/null +++ b/tests/t30012/test_case.h @@ -0,0 +1,70 @@ +/** + * tests/t30012/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("t30012", "[test-case][package]") +{ + auto [config, db] = load_config("t30012"); + + auto diagram = config.diagrams["t30012_package"]; + + REQUIRE(diagram->name == "t30012_package"); + + auto model = generate_package_diagram(*db, diagram); + + REQUIRE(model->name() == "t30012_package"); + + { + auto src = generate_package_puml(diagram, *model); + AliasMatcher _A(src); + + REQUIRE_THAT(src, StartsWith("@startuml")); + REQUIRE_THAT(src, EndsWith("@enduml\n")); + + // Check if all packages exist + REQUIRE_THAT(src, IsPackage("app")); + REQUIRE_THAT(src, IsPackage("lib1")); + REQUIRE_THAT(src, IsPackage("lib2")); + REQUIRE_THAT(src, IsPackage("mod1")); + REQUIRE_THAT(src, IsPackage("mod2")); + + save_puml(config.output_directory(), diagram->name + ".puml", src); + } + + { + auto j = generate_package_json(diagram, *model); + + using namespace json; + + save_json(config.output_directory(), diagram->name + ".json", j); + } + + { + auto src = generate_package_mermaid(diagram, *model); + + mermaid::AliasMatcher _A(src); + using mermaid::IsPackage; + + REQUIRE_THAT(src, IsPackage(_A("app"))); + REQUIRE_THAT(src, IsPackage(_A("lib1"))); + REQUIRE_THAT(src, IsPackage(_A("lib2"))); + REQUIRE_THAT(src, IsPackage(_A("mod1"))); + REQUIRE_THAT(src, IsPackage(_A("mod2"))); + + 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 8cefa23e..a8bf55b9 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -472,7 +472,9 @@ using namespace clanguml::test::matchers; #include "t30009/test_case.h" #include "t30010/test_case.h" #include "t30011/test_case.h" - +#if defined(ENABLE_CXX_MODULES_TEST_CASES) +#include "t30012/test_case.h" +#endif /// /// Include diagram tests /// diff --git a/util/templates/test_cases/test_case.h b/util/templates/test_cases/test_case.h index 17d2e3d2..23fdafde 100644 --- a/util/templates/test_cases/test_case.h +++ b/util/templates/test_cases/test_case.h @@ -50,7 +50,7 @@ TEST_CASE("{{ name }}", "[test-case][{{ type }}]") } { - auto src = generate_class_mermaid(diagram, *model); + auto src = generate_{{ type }}_mermaid(diagram, *model); mermaid::AliasMatcher _A(src); using mermaid::IsClass;