Added support for class diagram filtering based on C++20 modules (#195)

This commit is contained in:
Bartek Kryza
2023-12-17 20:49:41 +01:00
parent f2fe1ca2cf
commit ea6892f754
21 changed files with 310 additions and 21 deletions

View File

@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.12)
cmake_minimum_required(VERSION 3.16)
#
# Project name
@@ -153,6 +153,7 @@ add_subdirectory(src)
# Enable testing via CTest
#
option(BUILD_TESTS "" ON)
option(ENABLE_CXX_MODULES_TEST_CASES "" OFF)
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)

View File

@@ -35,6 +35,9 @@ LLVM_CONFIG_PATH ?=
CMAKE_PREFIX ?=
CMAKE_CXX_FLAGS ?=
CMAKE_EXE_LINKER_FLAGS ?=
CMAKE_GENERATOR ?= Unix Makefiles
ENABLE_CXX_MODULES_TEST_CASES ?= OFF
GIT_VERSION ?= $(shell git describe --tags --always --abbrev=7)
PKG_VERSION ?= $(shell git describe --tags --always --abbrev=7 | tr - .)
@@ -49,6 +52,7 @@ clean:
debug/CMakeLists.txt:
cmake -S . -B debug \
-G"$(CMAKE_GENERATOR)" \
-DGIT_VERSION=$(GIT_VERSION) \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_BUILD_TYPE=Debug \
@@ -56,10 +60,12 @@ debug/CMakeLists.txt:
-DCMAKE_EXE_LINKER_FLAGS="$(CMAKE_EXE_LINKER_FLAGS)" \
-DLLVM_VERSION=${LLVM_VERSION} \
-DLLVM_CONFIG_PATH=${LLVM_CONFIG_PATH} \
-DCMAKE_PREFIX=${CMAKE_PREFIX}
-DCMAKE_PREFIX=${CMAKE_PREFIX} \
-DENABLE_CXX_MODULES_TEST_CASES=$(ENABLE_CXX_MODULES_TEST_CASES)
release/CMakeLists.txt:
cmake -S . -B release \
-G"$(CMAKE_GENERATOR)" \
-DGIT_VERSION=$(GIT_VERSION) \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_BUILD_TYPE=Release \
@@ -67,10 +73,12 @@ release/CMakeLists.txt:
-DCMAKE_EXE_LINKER_FLAGS="$(CMAKE_EXE_LINKER_FLAGS)" \
-DLLVM_VERSION=${LLVM_VERSION} \
-DLLVM_CONFIG_PATH=${LLVM_CONFIG_PATH} \
-DCMAKE_PREFIX=${CMAKE_PREFIX}
-DCMAKE_PREFIX=${CMAKE_PREFIX} \
-DENABLE_CXX_MODULES_TEST_CASES=$(ENABLE_CXX_MODULES_TEST_CASES)
debug_tidy/CMakeLists.txt:
cmake -S . -B debug_tidy \
-G"$(CMAKE_GENERATOR)" \
-DGIT_VERSION=$(GIT_VERSION) \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_BUILD_TYPE=Debug \
@@ -79,24 +87,25 @@ debug_tidy/CMakeLists.txt:
-DCMAKE_EXE_LINKER_FLAGS="$(CMAKE_EXE_LINKER_FLAGS)" \
-DLLVM_VERSION=${LLVM_VERSION} \
-DLLVM_CONFIG_PATH=${LLVM_CONFIG_PATH} \
-DCMAKE_PREFIX=${CMAKE_PREFIX}
-DCMAKE_PREFIX=${CMAKE_PREFIX} \
-DENABLE_CXX_MODULES_TEST_CASES=$(ENABLE_CXX_MODULES_TEST_CASES)
debug: debug/CMakeLists.txt
echo "Using ${NUMPROC} cores"
make -C debug -j$(NUMPROC)
cmake --build debug -j$(NUMPROC)
debug_tidy: debug_tidy/CMakeLists.txt
echo "Using ${NUMPROC} cores"
make -C debug_tidy -j$(NUMPROC)
cmake --build debug_tidy -j$(NUMPROC)
release: release/CMakeLists.txt
make -C release -j$(NUMPROC)
cmake --build release -j$(NUMPROC)
test: debug
CTEST_OUTPUT_ON_FAILURE=1 make -C debug test
CTEST_OUTPUT_ON_FAILURE=1 ctest --test-dir debug
test_release: release
CTEST_OUTPUT_ON_FAILURE=1 make -C release test
CTEST_OUTPUT_ON_FAILURE=1 ctest --test-dir release
install: release
make -C release install DESTDIR=${DESTDIR}

View File

@@ -42,6 +42,7 @@ Main features supported so far include:
* Support for plain C99/C11 code (struct, units and their relationships) - [_example_](docs/test_cases/t00057.md)
* C++20 concept constraints - [_example_](docs/test_cases/t00059.md)
* C++20 coroutines - [_example_](docs/test_cases/t00069.md)
* Diagram content filtering based on C++20 modules - [_example_](docs/test_cases/t00070.md)
* **Sequence diagram generation**
* Generation of sequence diagram from specific method or function - [_example_](docs/test_cases/t20001.md)
* Generation of loop and conditional statements - [_example_](docs/test_cases/t20021.md)

View File

@@ -159,6 +159,7 @@ bool translation_unit_visitor::VisitEnumDecl(clang::EnumDecl *enm)
process_comment(*enm, e);
set_source_location(*enm, e);
set_owning_module(*enm, e);
if (e.skip())
return true;
@@ -265,6 +266,7 @@ bool translation_unit_visitor::VisitTypeAliasTemplateDecl(
LOG_DBG("Adding class {} with id {}", name, id);
set_source_location(*cls, *template_specialization_ptr);
set_owning_module(*cls, *template_specialization_ptr);
add_class(std::move(template_specialization_ptr));
}
@@ -677,6 +679,9 @@ bool translation_unit_visitor::VisitCXXRecordDecl(clang::CXXRecordDecl *cls)
LOG_DBG(
"== getQualifiedNameAsString() = {}", cls->getQualifiedNameAsString());
if (cls->getOwningModule() != nullptr)
LOG_DBG(
"== getOwningModule()->Name = {}", cls->getOwningModule()->Name);
LOG_DBG("== getID() = {}", cls->getID());
LOG_DBG("== isTemplateDecl() = {}", cls->isTemplateDecl());
LOG_DBG("== isTemplated() = {}", cls->isTemplated());
@@ -762,6 +767,7 @@ translation_unit_visitor::create_concept_declaration(clang::ConceptDecl *cpt)
process_comment(*cpt, concept_model);
set_source_location(*cpt, concept_model);
set_owning_module(*cpt, concept_model);
if (concept_model.skip())
return {};
@@ -802,6 +808,7 @@ std::unique_ptr<class_> translation_unit_visitor::create_record_declaration(
process_comment(*rec, record);
set_source_location(*rec, record);
set_owning_module(*rec, record);
const auto record_full_name = record_ptr->full_name(false);
@@ -841,6 +848,7 @@ std::unique_ptr<class_> translation_unit_visitor::create_class_declaration(
process_comment(*cls, c);
set_source_location(*cls, c);
set_owning_module(*cls, c);
if (c.skip())
return {};
@@ -1835,6 +1843,7 @@ translation_unit_visitor::process_template_specialization(
process_comment(*cls, template_instantiation);
set_source_location(*cls, template_instantiation);
set_owning_module(*cls, template_instantiation);
if (template_instantiation.skip())
return {};

View File

@@ -285,6 +285,39 @@ tvl::value_t namespace_filter::match(const diagram &d, const element &e) const
return result;
}
modules_filter::modules_filter(
filter_t type, std::vector<common::string_or_regex> modules)
: filter_visitor{type}
, modules_{std::move(modules)}
{
}
tvl::value_t modules_filter::match(const diagram &d, const element &e) const
{
if (modules_.empty())
return {};
if (!e.module().has_value())
return {false};
const auto module_toks = util::split(e.module().value(), ".");
auto result = tvl::any_of(modules_.begin(), modules_.end(),
[&e, &module_toks](const auto &modit) {
if (std::holds_alternative<std::string>(modit.value())) {
const auto &modit_str = std::get<std::string>(modit.value());
const auto modit_toks = util::split(modit_str, ".");
return e.module() == modit_str ||
util::starts_with(module_toks, modit_toks);
}
return std::get<common::regex>(modit.value()) %= e.module().value();
});
return result;
}
element_filter::element_filter(
filter_t type, std::vector<common::string_or_regex> elements)
: filter_visitor{type}
@@ -887,6 +920,9 @@ void diagram_filter::init_filters(const config::diagram &c)
add_inclusive_filter(std::make_unique<namespace_filter>(
filter_t::kInclusive, c.include().namespaces));
add_inclusive_filter(std::make_unique<modules_filter>(
filter_t::kInclusive, c.include().modules));
add_inclusive_filter(std::make_unique<relationship_filter>(
filter_t::kInclusive, c.include().relationships));
@@ -997,6 +1033,9 @@ void diagram_filter::init_filters(const config::diagram &c)
add_exclusive_filter(std::make_unique<namespace_filter>(
filter_t::kExclusive, c.exclude().namespaces));
add_exclusive_filter(std::make_unique<modules_filter>(
filter_t::kExclusive, c.exclude().modules));
add_exclusive_filter(std::make_unique<paths_filter>(
filter_t::kExclusive, c.root_directory(), c.exclude().paths));

View File

@@ -154,6 +154,21 @@ private:
std::vector<common::namespace_or_regex> namespaces_;
};
/**
* Match diagram elements to a set of specified modules or
* module regex patterns.
*/
struct modules_filter : public filter_visitor {
modules_filter(filter_t type, std::vector<common::string_or_regex> modules);
~modules_filter() override = default;
tvl::value_t match(const diagram &d, const element &e) const override;
private:
std::vector<common::string_or_regex> modules_;
};
/**
* Match element's name to a set of names or regex patterns.
*/

View File

@@ -87,6 +87,20 @@ public:
*/
const namespace_ &path() const { return ns_; }
/**
* Set elements owning module.
*
* @param module C++20 module.
*/
void set_module(const std::string &module) { module_ = module; }
/**
* Return elements owning module, if any.
*
* @return C++20 module.
*/
std::optional<std::string> module() const { return module_; }
/**
* Return elements full name.
*
@@ -120,5 +134,6 @@ public:
private:
namespace_ ns_;
namespace_ using_namespace_;
std::optional<std::string> module_;
};
} // namespace clanguml::common::model

View File

@@ -21,6 +21,8 @@
#include "comment/clang_visitor.h"
#include "comment/plain_visitor.h"
#include "clang/Basic/Module.h"
namespace clanguml::common::visitor {
translation_unit_visitor::translation_unit_visitor(
@@ -161,4 +163,12 @@ void translation_unit_visitor::set_source_location(
element.set_location_id(location.getHashValue());
}
void translation_unit_visitor::set_owning_module(
const clang::Decl &decl, clanguml::common::model::element &element)
{
if (const clang::Module *module = decl.getOwningModule();
module != nullptr) {
element.set_module(module->Name);
}
}
} // namespace clanguml::common::visitor

View File

@@ -100,6 +100,9 @@ public:
void set_source_location(const clang::SourceLocation &location,
clanguml::common::model::source_location &element);
void set_owning_module(
const clang::Decl &decl, clanguml::common::model::element &element);
protected:
/**
* @brief Process comment directives in comment attached to a declaration

View File

@@ -180,6 +180,19 @@ struct filter {
*/
std::vector<common::namespace_or_regex> namespaces;
/*! @brief Modules filter
*
* Example:
*
* ```yaml
* include
* modules:
* - app.module1
* - r: ".*internal.*"
* ```
*/
std::vector<common::string_or_regex> modules;
/*! @brief Elements filter
*
* Example:

View File

@@ -64,10 +64,6 @@ types:
regex_t:
r: string
regex_or_string_t: [string, regex_t]
namespaces_filter_t:
namespaces: [regex_or_string_t]
elements_filter_t:
elements: [regex_or_string_t]
element_types_filter_t: !variant
- class
- enum
@@ -121,6 +117,7 @@ types:
- context_filter_match_t
filter_t:
namespaces: !optional [regex_or_string_t]
modules: !optional [regex_or_string_t]
elements: !optional [regex_or_string_t]
element_types: !optional [element_types_filter_t]
relationships: !optional [relationship_filter_t]

View File

@@ -475,6 +475,12 @@ template <> struct convert<filter> {
rhs.namespaces.push_back({ns});
}
if (node["modules"]) {
auto module_list = node["modules"].as<decltype(rhs.modules)>();
for (const auto &ns : module_list)
rhs.modules.push_back({ns});
}
if (node["relationships"])
rhs.relationships =
node["relationships"].as<decltype(rhs.relationships)>();

View File

@@ -122,6 +122,8 @@ YAML::Emitter &operator<<(YAML::Emitter &out, const filter &f)
out << YAML::BeginMap;
if (!f.namespaces.empty())
out << YAML::Key << "namespaces" << YAML::Value << f.namespaces;
if (!f.modules.empty())
out << YAML::Key << "modules" << YAML::Value << f.modules;
if (!f.access.empty())
out << YAML::Key << "access" << YAML::Value << f.access;
if (!f.context.empty())

View File

@@ -1,11 +1,30 @@
file(GLOB_RECURSE TEST_CASE_SOURCES t*/*.cc t*/*.c t*/src/*.c)
file(GLOB_RECURSE TEST_CASE_MODULE_SOURCES t*/src/*.cppm)
file(GLOB_RECURSE TEST_CASE_CONFIGS t*/.clang-uml)
file(GLOB_RECURSE TEST_CONFIG_YMLS test_config_data/*.yml
test_compilation_database_data/*.yml
test_compilation_database_data/*.json)
set(TEST_CASES_REQUIRING_CXX20 t00056 t00058 t00059 t00065 t00069)
set(TEST_CASES_REQUIRING_CXX20_MODULES t00070)
if(ENABLE_CXX_MODULES_TEST_CASES)
foreach(CXX20_MOD_TC ${TEST_CASES_REQUIRING_CXX20_MODULES})
list(APPEND TEST_CASES_REQUIRING_CXX20 ${CXX20_MOD_TC})
endforeach()
set(CMAKE_CXX_SCAN_FOR_MODULES ON)
else()
foreach(CXX20_MOD_TC ${TEST_CASES_REQUIRING_CXX20_MODULES})
list(FILTER TEST_CASE_SOURCES
EXCLUDE
REGEX ".*${CXX20_MOD_TC}.*")
list(FILTER TEST_CASE_CONFIGS
EXCLUDE
REGEX ".*${CXX20_MOD_TC}.*")
endforeach()
endif(ENABLE_CXX_MODULES_TEST_CASES)
set(CLANG_UML_TEST_LIBRARIES
clang-umllib
@@ -19,6 +38,7 @@ endif(MSVC)
list(FIND CMAKE_CXX_COMPILE_FEATURES cxx_std_20 SUPPORTS_CXX_STD_20)
# Remove test cases which require C++20 if they are not supported here
if(SUPPORTS_CXX_STD_20 EQUAL -1
OR ${LLVM_PACKAGE_VERSION} VERSION_LESS "14.0")
set(ENABLE_CXX_STD_20_TEST_CASES 0)
@@ -34,7 +54,7 @@ else()
set(ENABLE_CXX_STD_20_TEST_CASES 1)
endif()
set(TEST_CASES
set(TEST_NAMES
test_util
test_model
test_cases
@@ -46,15 +66,24 @@ set(TEST_CASES
test_thread_pool_executor
test_query_driver_output_extractor)
foreach(TEST_NAME ${TEST_CASES})
add_executable(${TEST_NAME}
${TEST_NAME}.cc
$<$<STREQUAL:${TEST_NAME},test_cases>:${TEST_CASE_SOURCES}>
catch.h)
foreach(TEST_NAME ${TEST_NAMES})
add_executable(${TEST_NAME})
if(TEST_NAME STREQUAL "test_cases")
if(ENABLE_CXX_MODULES_TEST_CASES)
target_sources(${TEST_NAME} PUBLIC FILE_SET CXX_MODULES FILES
${TEST_CASE_MODULE_SOURCES})
endif(ENABLE_CXX_MODULES_TEST_CASES)
target_sources(${TEST_NAME} PUBLIC ${TEST_NAME}.cc
${TEST_CASE_SOURCES} catch.h)
else()
target_sources(${TEST_NAME} PUBLIC ${TEST_NAME}.cc catch.h)
endif(TEST_NAME STREQUAL "test_cases")
target_compile_features(${TEST_NAME} PRIVATE
$<IF:${ENABLE_CXX_STD_20_TEST_CASES},cxx_std_20,cxx_std_17>)
target_compile_definitions(${TEST_NAME} PRIVATE
$<$<EQUAL:${ENABLE_CXX_STD_20_TEST_CASES},1>:ENABLE_CXX_STD_20_TEST_CASES>)
$<$<EQUAL:${ENABLE_CXX_STD_20_TEST_CASES},1>:ENABLE_CXX_STD_20_TEST_CASES>
$<$<BOOL:${ENABLE_CXX_MODULES_TEST_CASES}>:ENABLE_CXX_MODULES_TEST_CASES>)
target_compile_options(${TEST_NAME} PRIVATE
$<$<COMPILE_LANGUAGE:CXX>:
$<$<OR:$<CXX_COMPILER_ID:Clang>,$<CXX_COMPILER_ID:GNU>>:
@@ -89,6 +118,6 @@ foreach(TEST_CONFIG_YML ${TEST_CONFIG_YMLS})
COPYONLY)
endforeach()
foreach(TEST_NAME ${TEST_CASES})
foreach(TEST_NAME ${TEST_NAMES})
add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME})
endforeach()

12
tests/t00070/.clang-uml Normal file
View File

@@ -0,0 +1,12 @@
diagrams:
t00070_class:
type: class
glob:
- t00070.cc
include:
modules:
- t00070
exclude:
modules:
- t00070.lib2
using_namespace: clanguml::t00070

View File

@@ -0,0 +1,11 @@
export module t00070;
export import t00070.lib1;
export import t00070.lib2;
export namespace clanguml::t00070 {
class A {
int get() { return a; }
int a;
};
}

View File

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

View File

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

14
tests/t00070/t00070.cc Normal file
View File

@@ -0,0 +1,14 @@
import t00070.lib1;
import t00070.lib2;
namespace clanguml {
namespace t00070 {
int tmain()
{
B b;
C c;
return 0;
}
}
}

78
tests/t00070/test_case.h Normal file
View File

@@ -0,0 +1,78 @@
/**
* tests/t00070/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("t00070", "[test-case][class]")
{
auto [config, db] = load_config("t00070");
auto diagram = config.diagrams["t00070_class"];
REQUIRE(diagram->name == "t00070_class");
auto model = generate_class_diagram(*db, diagram);
REQUIRE(model->name() == "t00070_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("B")));
REQUIRE_THAT(src, !IsClass(_A("C")));
REQUIRE_THAT(src, IsClassTemplate("BB", "T"));
REQUIRE_THAT(src, !IsClassTemplate("CC", "T"));
REQUIRE_THAT(src, IsEnum(_A("BBB")));
REQUIRE_THAT(src, !IsEnum(_A("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("B")));
REQUIRE_THAT(src, !IsClass(_A("C")));
REQUIRE_THAT(src, IsClass(_A("BB<T>")));
REQUIRE_THAT(src, !IsClass(_A("CC<T>")));
REQUIRE_THAT(src, IsEnum(_A("BBB")));
REQUIRE_THAT(src, !IsEnum(_A("CCC")));
save_mermaid(config.output_directory(), diagram->name + ".mmd", src);
}
}

View File

@@ -407,6 +407,9 @@ using namespace clanguml::test::matchers;
#if defined(ENABLE_CXX_STD_20_TEST_CASES)
#include "t00069/test_case.h"
#endif
#if defined(ENABLE_CXX_MODULES_TEST_CASES)
#include "t00070/test_case.h"
#endif
///
/// Sequence diagram tests