From f2fe1ca2cf2dea82e4e4429201eb9af62d04d62b Mon Sep 17 00:00:00 2001 From: Bartek Kryza Date: Fri, 15 Dec 2023 20:00:56 +0100 Subject: [PATCH] Added support for C++20 coroutines in class diagrams (#221) --- README.md | 1 + .../json/class_diagram_generator.cc | 1 + .../mermaid/class_diagram_generator.cc | 3 + .../plantuml/class_diagram_generator.cc | 3 + src/class_diagram/model/class_method.cc | 7 ++ src/class_diagram/model/class_method.h | 15 ++++ .../visitor/translation_unit_visitor.cc | 1 + src/common/clang_utils.cc | 6 ++ src/common/clang_utils.h | 8 ++ tests/CMakeLists.txt | 2 +- tests/t00069/.clang-uml | 9 ++ tests/t00069/t00069.cc | 63 ++++++++++++++ tests/t00069/test_case.h | 87 +++++++++++++++++++ tests/test_cases.cc | 3 + tests/test_cases.h | 7 ++ tests/test_cases.yaml | 3 + 16 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 tests/t00069/.clang-uml create mode 100644 tests/t00069/t00069.cc create mode 100644 tests/t00069/test_case.h diff --git a/README.md b/README.md index e37295e3..4fcd8feb 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Main features supported so far include: * Interactive links to online code or docs for classes, methods and class fields in SVG diagrams - [_example_](https://raw.githubusercontent.com/bkryza/clang-uml/master/docs/test_cases/t00002_class.svg) * 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) * **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) diff --git a/src/class_diagram/generators/json/class_diagram_generator.cc b/src/class_diagram/generators/json/class_diagram_generator.cc index 53a89c9b..21bbe020 100644 --- a/src/class_diagram/generators/json/class_diagram_generator.cc +++ b/src/class_diagram/generators/json/class_diagram_generator.cc @@ -63,6 +63,7 @@ void to_json(nlohmann::json &j, const class_method &c) j["is_noexcept"] = c.is_noexcept(); j["is_constexpr"] = c.is_constexpr(); j["is_consteval"] = c.is_consteval(); + j["is_coroutine"] = c.is_coroutine(); j["is_constructor"] = c.is_constructor(); j["is_move_assignment"] = c.is_move_assignment(); j["is_copy_assignment"] = c.is_copy_assignment(); diff --git a/src/class_diagram/generators/mermaid/class_diagram_generator.cc b/src/class_diagram/generators/mermaid/class_diagram_generator.cc index 0b115042..b14b4483 100644 --- a/src/class_diagram/generators/mermaid/class_diagram_generator.cc +++ b/src/class_diagram/generators/mermaid/class_diagram_generator.cc @@ -251,6 +251,9 @@ void generator::generate_method( if (m.is_consteval()) { method_mods.emplace_back("consteval"); } + if (m.is_coroutine()) { + method_mods.emplace_back("coroutine"); + } if (!method_mods.empty()) { ostr << fmt::format("[{}] ", fmt::join(method_mods, ",")); diff --git a/src/class_diagram/generators/plantuml/class_diagram_generator.cc b/src/class_diagram/generators/plantuml/class_diagram_generator.cc index afd43db8..35c299e7 100644 --- a/src/class_diagram/generators/plantuml/class_diagram_generator.cc +++ b/src/class_diagram/generators/plantuml/class_diagram_generator.cc @@ -338,6 +338,9 @@ void generator::generate_method( else if (m.is_deleted()) ostr << " = deleted"; + if (m.is_coroutine()) + ostr << " [coroutine]"; + ostr << " : " << type; if (config().generate_links) { diff --git a/src/class_diagram/model/class_method.cc b/src/class_diagram/model/class_method.cc index d44ab43e..20f8f72d 100644 --- a/src/class_diagram/model/class_method.cc +++ b/src/class_diagram/model/class_method.cc @@ -70,6 +70,13 @@ void class_method::is_consteval(bool is_consteval) is_consteval_ = is_consteval; } +bool class_method::is_coroutine() const { return is_coroutine_; } + +void class_method::is_coroutine(bool is_coroutine) +{ + is_coroutine_ = is_coroutine; +} + bool class_method::is_noexcept() const { return is_noexcept_; } void class_method::is_noexcept(bool is_noexcept) { is_noexcept_ = is_noexcept; } diff --git a/src/class_diagram/model/class_method.h b/src/class_diagram/model/class_method.h index 8f89f5ae..0e188331 100644 --- a/src/class_diagram/model/class_method.h +++ b/src/class_diagram/model/class_method.h @@ -152,6 +152,20 @@ public: */ void is_consteval(bool is_consteval); + /** + * @brief Whether the method is a C++20 coroutine. + * + * @return True, if the method is a coroutine + */ + bool is_coroutine() const; + + /** + * @brief Set whether the method is a C++20 coroutine. + * + * @param is_coroutine True, if the method is a coroutine + */ + void is_coroutine(bool is_coroutine); + /** * @brief Whether the method is noexcept. * @@ -262,6 +276,7 @@ private: bool is_noexcept_{false}; bool is_constexpr_{false}; bool is_consteval_{false}; + bool is_coroutine_{false}; bool is_constructor_{false}; bool is_destructor_{false}; bool is_move_assignment_{false}; diff --git a/src/class_diagram/visitor/translation_unit_visitor.cc b/src/class_diagram/visitor/translation_unit_visitor.cc index 2b30b4d5..2a19423e 100644 --- a/src/class_diagram/visitor/translation_unit_visitor.cc +++ b/src/class_diagram/visitor/translation_unit_visitor.cc @@ -1369,6 +1369,7 @@ void translation_unit_visitor::process_method_properties( method.is_move_assignment(mf.isMoveAssignmentOperator()); method.is_copy_assignment(mf.isCopyAssignmentOperator()); method.is_noexcept(isNoexceptExceptionSpec(mf.getExceptionSpecType())); + method.is_coroutine(common::is_coroutine(mf)); } void translation_unit_visitor:: diff --git a/src/common/clang_utils.cc b/src/common/clang_utils.cc index 22879a3e..44e4e3f7 100644 --- a/src/common/clang_utils.cc +++ b/src/common/clang_utils.cc @@ -876,4 +876,10 @@ clang::RawComment *get_expression_raw_comment(const clang::SourceManager &sm, return {}; } +bool is_coroutine(const clang::FunctionDecl &decl) +{ + const auto *body = decl.getBody(); + return clang::isa_and_nonnull(body); +} + } // namespace clanguml::common diff --git a/src/common/clang_utils.h b/src/common/clang_utils.h index 3dddeeb4..f94c7b4c 100644 --- a/src/common/clang_utils.h +++ b/src/common/clang_utils.h @@ -295,4 +295,12 @@ consume_type_context(clang::QualType type); clang::RawComment *get_expression_raw_comment(const clang::SourceManager &sm, const clang::ASTContext &context, const clang::Stmt *stmt); +/** + * Check if function or method declaration is a C++20 coroutine. + * + * @param decl Function declaration + * @return True, if the function is a C++20 coroutine. + */ +bool is_coroutine(const clang::FunctionDecl &decl); + } // namespace clanguml::common diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 969967b0..0fcb00f8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,7 +5,7 @@ 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) +set(TEST_CASES_REQUIRING_CXX20 t00056 t00058 t00059 t00065 t00069) set(CLANG_UML_TEST_LIBRARIES clang-umllib diff --git a/tests/t00069/.clang-uml b/tests/t00069/.clang-uml new file mode 100644 index 00000000..8b8d8450 --- /dev/null +++ b/tests/t00069/.clang-uml @@ -0,0 +1,9 @@ +diagrams: + t00069_class: + type: class + glob: + - t00069.cc + include: + namespaces: + - clanguml::t00069 + using_namespace: clanguml::t00069 \ No newline at end of file diff --git a/tests/t00069/t00069.cc b/tests/t00069/t00069.cc new file mode 100644 index 00000000..606b60b3 --- /dev/null +++ b/tests/t00069/t00069.cc @@ -0,0 +1,63 @@ +#include +#include + +namespace clanguml { +namespace t00069 { + +template struct generator { + struct promise_type; + using handle_type = std::coroutine_handle; + + generator(handle_type h) + : h_(h) + { + } + + ~generator() { h_.destroy(); } + + struct promise_type { + T value_; + std::exception_ptr exception_; + + generator get_return_object() + { + return generator(handle_type::from_promise(*this)); + } + std::suspend_always initial_suspend() { return {}; } + + std::suspend_always final_suspend() noexcept { return {}; } + + void unhandled_exception() { exception_ = std::current_exception(); } + + template From> + std::suspend_always yield_value(From &&from) + { + value_ = std::forward(from); + return {}; + } + + void return_void() { } + }; + + handle_type h_; + +private: + bool full_ = false; +}; + +class A { +public: + generator iota() { co_yield counter_++; } + + generator seed() + { + counter_ = 42; + co_return; + } + +private: + unsigned long counter_; +}; + +} // namespace t00069 +} // namespace clanguml diff --git a/tests/t00069/test_case.h b/tests/t00069/test_case.h new file mode 100644 index 00000000..98894ea6 --- /dev/null +++ b/tests/t00069/test_case.h @@ -0,0 +1,87 @@ +/** + * tests/t00069/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("t00069", "[test-case][class]") +{ + auto [config, db] = load_config("t00069"); + + auto diagram = config.diagrams["t00069_class"]; + + REQUIRE(diagram->name == "t00069_class"); + + auto model = generate_class_diagram(*db, diagram); + + REQUIRE(model->name() == "t00069_class"); + + { + auto src = generate_class_puml(diagram, *model); + AliasMatcher _A(src); + + REQUIRE_THAT(src, StartsWith("@startuml")); + REQUIRE_THAT(src, EndsWith("@enduml\n")); + + // Check if all classes exist + REQUIRE_THAT(src, IsClass(_A("A"))); + + // Check if class templates exist + REQUIRE_THAT(src, IsClassTemplate("generator", "T")); + + // Check if all inner classes exist + REQUIRE_THAT(src, + IsInnerClass(_A("generator"), _A("generator::promise_type"))); + + // Check if all methods exist + REQUIRE_THAT(src, + (IsMethod("iota", "generator"))); + REQUIRE_THAT(src, + (IsMethod("seed", "generator"))); + + // Check if all relationships exist + REQUIRE_THAT( + src, IsDependency(_A("A"), _A("generator"))); + REQUIRE_THAT(src, + IsInstantiation( + _A("generator"), _A("generator"))); + + 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::IsMethod; + + REQUIRE_THAT(src, IsClass(_A("A"))); + REQUIRE_THAT(src, + (IsMethod("iota", "generator"))); + REQUIRE_THAT(src, + (IsMethod("seed", "generator"))); + + 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 6a2a672b..4b7f95f5 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -404,6 +404,9 @@ using namespace clanguml::test::matchers; #include "t00066/test_case.h" #include "t00067/test_case.h" #include "t00068/test_case.h" +#if defined(ENABLE_CXX_STD_20_TEST_CASES) +#include "t00069/test_case.h" +#endif /// /// Sequence diagram tests diff --git a/tests/test_cases.h b/tests/test_cases.h index 0e3a9c99..bf4e9249 100644 --- a/tests/test_cases.h +++ b/tests/test_cases.h @@ -108,6 +108,8 @@ struct Constexpr { }; struct Consteval { }; +struct Coroutine { }; + struct Noexcept { }; struct Default { }; @@ -962,6 +964,9 @@ ContainsMatcher IsMethod(std::string const &name, if constexpr (has_type()) pattern += " = deleted"; + if constexpr (has_type()) + pattern += " [coroutine]"; + pattern += " : " + type; return ContainsMatcher(CasedString(pattern, caseSensitivity)); @@ -995,6 +1000,8 @@ ContainsMatcher IsMethod(std::string const &name, std::string type = "void", method_mods.push_back("constexpr"); if constexpr (has_type()) method_mods.push_back("consteval"); + if constexpr (has_type()) + method_mods.push_back("coroutine"); pattern += " : "; diff --git a/tests/test_cases.yaml b/tests/test_cases.yaml index 7b7c584e..c212aa5a 100644 --- a/tests/test_cases.yaml +++ b/tests/test_cases.yaml @@ -201,6 +201,9 @@ test_cases: - name: t00068 title: Context filter radius parameter test case description: + - name: t00069 + title: Coroutine methods in class diagrams + description: Sequence diagrams: - name: t20001 title: Basic sequence diagram test case