Added package diagram generation from C++20 modules (#101)

This commit is contained in:
Bartek Kryza
2023-12-21 23:39:59 +01:00
parent f09edd8b47
commit a8d646d1bc
18 changed files with 285 additions and 12 deletions

View File

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

View File

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

View File

@@ -21,8 +21,8 @@
#include <sstream>
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}
{
}

View File

@@ -41,7 +41,8 @@ class package : public element,
public stylable_element,
public nested_trait<element, path> {
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;

View File

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

View File

@@ -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 <typename ElementT>
bool add_with_module_path(
const common::model::path &parent_path, std::unique_ptr<ElementT> &&e);
/**
* @brief Add element using namespace as diagram path
*
@@ -237,6 +251,55 @@ bool diagram::add_with_namespace_path(std::unique_ptr<ElementT> &&p)
return res;
}
template <typename ElementT>
bool diagram::add_with_module_path(
const common::model::path &parent_path, std::unique_ptr<ElementT> &&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<common::model::package>(
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<ElementT>::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<ElementT>::add(p_ref);
return res;
}
template <typename ElementT>
bool diagram::add_with_filesystem_path(
const common::model::path &parent_path, std::unique_ptr<ElementT> &&p)

View File

@@ -21,6 +21,8 @@
#include "common/clang_utils.h"
#include "common/model/namespace.h"
#include "clang/Basic/Module.h"
#include <spdlog/spdlog.h>
#include <deque>
@@ -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<common::model::package>(
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)})) {

View File

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

10
tests/t30012/.clang-uml Normal file
View File

@@ -0,0 +1,10 @@
diagrams:
t30012_package:
type: package
glob:
- t30012.cc
package_type: module
include:
modules:
- t30012
using_module: t30012

View File

@@ -0,0 +1,13 @@
export module t30012.app.lib1;
export namespace clanguml::t30012 {
class B { };
template <typename T> class BB {
T t;
};
namespace detail {
enum class BBB { bbb1, bbb2 };
} // namespace detail
}

View File

@@ -0,0 +1,5 @@
export module t30012.app.lib1.mod1;
export namespace clanguml::t30012 {
class D { };
}

View File

@@ -0,0 +1,5 @@
export module t30012.app.lib1.mod2;
export namespace clanguml::t30012 {
class E { };
}

View File

@@ -0,0 +1,13 @@
export module t30012.app.lib2;
export namespace clanguml::t30012 {
class C { };
template <typename T> class CC {
T t;
};
namespace detail {
enum class CCC { ccc1, ccc2 };
}
}

View File

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

15
tests/t30012/t30012.cc Normal file
View File

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

70
tests/t30012/test_case.h Normal file
View File

@@ -0,0 +1,70 @@
/**
* tests/t30012/test_case.h
*
* Copyright (c) 2021-2023 Bartek Kryza <bkryza@gmail.com>
*
* 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);
}
}

View File

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

View File

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