From 9c07dbfde33ff6687e98ff61488557874ab6ed33 Mon Sep 17 00:00:00 2001 From: Bartek Kryza Date: Sat, 20 Apr 2024 23:16:02 +0200 Subject: [PATCH] Added support for call expressions tracking through lambdas in function arguments (#168) --- .../plantuml/sequence_diagram_generator.cc | 6 +- src/sequence_diagram/model/participant.h | 7 +- .../visitor/translation_unit_visitor.cc | 244 +++++++++++++++--- .../visitor/translation_unit_visitor.h | 28 ++ tests/t20044/.clang-uml | 17 ++ tests/t20044/t20044.cc | 100 +++++++ tests/t20044/test_case.h | 91 +++++++ tests/test_cases.cc | 1 + uml/class/class_translation_unit_visitor.yml | 2 +- 9 files changed, 452 insertions(+), 44 deletions(-) create mode 100644 tests/t20044/.clang-uml create mode 100644 tests/t20044/t20044.cc create mode 100644 tests/t20044/test_case.h diff --git a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc index 7d97a80e..64ed970a 100644 --- a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc +++ b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc @@ -112,11 +112,15 @@ void generator::generate_call(const message &m, std::ostream &ostr) const void generator::generate_return(const message &m, std::ostream &ostr) const { + // Add return activity only for messages between different actors and // only if the return type is different than void + if (m.from() == m.to()) + return; + const auto &from = model().get_participant(m.from()); const auto &to = model().get_participant(m.to()); - if ((m.from() != m.to()) && !to.value().is_void()) { + if (to.has_value() && !to.value().is_void()) { const std::string from_alias = generate_alias(from.value()); const std::string to_alias = generate_alias(to.value()); diff --git a/src/sequence_diagram/model/participant.h b/src/sequence_diagram/model/participant.h index 950a6f26..68eaeade 100644 --- a/src/sequence_diagram/model/participant.h +++ b/src/sequence_diagram/model/participant.h @@ -190,12 +190,17 @@ public: */ void is_lambda(bool is_lambda); + void set_lambda_operator_id(common::id_t id) { lambda_operator_id_ = id; } + + common::id_t lambda_operator_id() const { return lambda_operator_id_; } + private: bool is_struct_{false}; bool is_template_{false}; bool is_template_instantiation_{false}; bool is_alias_{false}; bool is_lambda_{false}; + common::id_t lambda_operator_id_{0}; std::string full_name_; }; @@ -406,7 +411,7 @@ struct method : public function { * * @return Fully qualified elements name. */ - std::string full_name(bool /*relative*/) const override; + std::string full_name(bool relative) const override; std::string message_name(message_render_mode mode) const override; diff --git a/src/sequence_diagram/visitor/translation_unit_visitor.cc b/src/sequence_diagram/visitor/translation_unit_visitor.cc index 33b5d558..e6ee6651 100644 --- a/src/sequence_diagram/visitor/translation_unit_visitor.cc +++ b/src/sequence_diagram/visitor/translation_unit_visitor.cc @@ -217,6 +217,22 @@ bool translation_unit_visitor::VisitClassTemplateSpecializationDecl( return true; } +bool translation_unit_visitor::TraverseCXXMethodDecl( + clang::CXXMethodDecl *declaration) +{ + // We need to backup the context, since other methods or functions can + // be traversed during this traversal (e.g. template function/method + // specializations) + auto context_backup = context(); + + RecursiveASTVisitor::TraverseCXXMethodDecl( + declaration); + + call_expression_context_ = context_backup; + + return true; +} + bool translation_unit_visitor::VisitCXXMethodDecl( clang::CXXMethodDecl *declaration) { @@ -229,6 +245,9 @@ bool translation_unit_visitor::VisitCXXMethodDecl( if (auto *method_definition = clang::dyn_cast( declaration_definition); method_definition != nullptr) { + LOG_DBG("Calling VisitCXXMethodDecl recursively for forward " + "declaration"); + return VisitCXXMethodDecl(method_definition); } } @@ -255,7 +274,7 @@ bool translation_unit_visitor::VisitCXXMethodDecl( method_model_ptr->set_id(common::to_id(method_full_name)); // Callee methods in call expressions are referred to by first declaration - // id + // id, so they should both be mapped to method_model if (declaration->isThisDeclarationADefinition()) { set_unique_id( declaration->getFirstDecl()->getID(), method_model_ptr->id()); @@ -276,6 +295,22 @@ bool translation_unit_visitor::VisitCXXMethodDecl( return true; } +bool translation_unit_visitor::TraverseFunctionDecl( + clang::FunctionDecl *declaration) +{ + // We need to backup the context, since other methods or functions can + // be traversed during this traversal (e.g. template function/method + // specializations) + auto context_backup = context(); + + RecursiveASTVisitor::TraverseFunctionDecl( + declaration); + + call_expression_context_ = context_backup; + + return true; +} + bool translation_unit_visitor::VisitFunctionDecl( clang::FunctionDecl *declaration) { @@ -391,8 +426,9 @@ bool translation_unit_visitor::VisitLambdaExpr(clang::LambdaExpr *expr) const auto lambda_full_name = expr->getLambdaClass()->getCanonicalDecl()->getNameAsString(); - LOG_TRACE("Visiting lambda expression {} at {}", lambda_full_name, - expr->getBeginLoc().printToString(source_manager())); + LOG_TRACE("Visiting lambda expression {} at {} [caller_id = {}]", + lambda_full_name, expr->getBeginLoc().printToString(source_manager()), + context().caller_id()); LOG_TRACE("Lambda call operator ID {} - lambda class ID {}, class call " "operator ID {}", @@ -427,6 +463,33 @@ bool translation_unit_visitor::VisitLambdaExpr(clang::LambdaExpr *expr) lambda_method_model_ptr->set_id(common::to_id( get_participant(cls_id).value().full_name(false) + "::" + method_name)); + get_participant(cls_id).value().set_lambda_operator_id( + lambda_method_model_ptr->id()); + + // If lambda expression is in an argument to a method/function, and that + // method function would be excluded by filters + if (std::holds_alternative( + context().current_callexpr())/* && + !should_include( + std::get(context().current_callexpr()))*/) { + using clanguml::common::model::message_t; + using clanguml::sequence_diagram::model::message; + + message m{message_t::kCall, context().caller_id()}; + set_source_location(*expr, m); + m.set_from(context().caller_id()); + m.set_to(lambda_method_model_ptr->id()); + + diagram().add_active_participant(m.from()); + diagram().add_active_participant(m.to()); + + LOG_DBG("Found call {} from {} [{}] to {} [{}]", m.message_name(), + m.from(), m.from(), m.to(), m.to()); + + push_message(std::get(context().current_callexpr()), + std::move(m)); + } + context().enter_lambda_expression(lambda_method_model_ptr->id()); set_unique_id( @@ -454,10 +517,19 @@ bool translation_unit_visitor::TraverseLambdaExpr(clang::LambdaExpr *expr) bool translation_unit_visitor::TraverseCallExpr(clang::CallExpr *expr) { + if (source_manager().isInSystemHeader(expr->getSourceRange().getBegin())) + return true; + + LOG_DBG("Entering call expression at {}", + expr->getBeginLoc().printToString(source_manager())); + context().enter_callexpr(expr); RecursiveASTVisitor::TraverseCallExpr(expr); + LOG_DBG("Leaving call expression at {}", + expr->getBeginLoc().printToString(source_manager())); + context().leave_callexpr(); pop_message_to_diagram(expr); @@ -468,9 +540,24 @@ bool translation_unit_visitor::TraverseCallExpr(clang::CallExpr *expr) bool translation_unit_visitor::TraverseCXXMemberCallExpr( clang::CXXMemberCallExpr *expr) { + if (source_manager().isInSystemHeader(expr->getSourceRange().getBegin())) + return true; + + LOG_DBG("Entering member call expression at {} to {}::{}", + expr->getBeginLoc().printToString(source_manager()), + common::to_string(expr->getObjectType(), context().get_ast_context()), + common::to_string(expr->getMethodDecl())); + + context().enter_callexpr(expr); + RecursiveASTVisitor::TraverseCXXMemberCallExpr( expr); + LOG_DBG("Leaving member call expression at {}", + expr->getBeginLoc().printToString(source_manager())); + + context().leave_callexpr(); + pop_message_to_diagram(expr); return true; @@ -479,9 +566,13 @@ bool translation_unit_visitor::TraverseCXXMemberCallExpr( bool translation_unit_visitor::TraverseCXXOperatorCallExpr( clang::CXXOperatorCallExpr *expr) { + context().enter_callexpr(expr); + RecursiveASTVisitor::TraverseCXXOperatorCallExpr( expr); + context().leave_callexpr(); + pop_message_to_diagram(expr); return true; @@ -944,6 +1035,10 @@ bool translation_unit_visitor::VisitCallExpr(clang::CallExpr *expr) if (!context().valid() || context().get_ast_context() == nullptr) return true; + LOG_TRACE("Visiting call expression at {} [caller_id = {}]", + expr->getBeginLoc().printToString(source_manager()), + context().caller_id()); + message m{message_t::kCall, context().caller_id()}; m.in_static_declaration_context(within_static_variable_declaration_ > 0); @@ -957,25 +1052,11 @@ bool translation_unit_visitor::VisitCallExpr(clang::CallExpr *expr) if (m.skip()) return true; - auto generated_message_from_comment{false}; - for (const auto &decorator : m.decorators()) { - auto call_decorator = - std::dynamic_pointer_cast(decorator); - if (call_decorator && - call_decorator->applies_to_diagram(config().name)) { - m.set_to(common::to_id(call_decorator->callee)); - generated_message_from_comment = true; - break; - } - } + auto generated_message_from_comment = generate_message_from_comment(m); if (!generated_message_from_comment && !should_include(expr)) return true; - LOG_TRACE("Visiting call expression at {} [caller_id = {}]", - expr->getBeginLoc().printToString(source_manager()), - context().caller_id()); - // If we're currently inside a lambda expression, set it's id as // message source rather then enclosing context // Unless the lambda is declared in a function or method call @@ -987,7 +1068,9 @@ bool translation_unit_visitor::VisitCallExpr(clang::CallExpr *expr) m.set_message_scope(common::model::message_scope_t::kCondition); } - if (generated_message_from_comment) { } + if (generated_message_from_comment) { + // Do nothing + } // // Call to an overloaded operator // @@ -1015,8 +1098,11 @@ bool translation_unit_visitor::VisitCallExpr(clang::CallExpr *expr) auto *callee_decl = expr->getCalleeDecl(); if (callee_decl == nullptr) { - LOG_DBG("Cannot get callee declaration - trying direct callee..."); + LOG_DBG("Cannot get callee declaration - trying direct function " + "callee..."); callee_decl = expr->getDirectCallee(); + LOG_DBG( + "Found function/method callee in: {}", common::to_string(expr)); } if (callee_decl == nullptr) { @@ -1040,9 +1126,10 @@ bool translation_unit_visitor::VisitCallExpr(clang::CallExpr *expr) } } else { - if (!process_function_call_expression(m, expr)) { - LOG_DBG("Skipping call to unsupported type of call expression " - "at: {}", + auto success = process_function_call_expression(m, expr); + + if (!success) { + LOG_DBG("Skipping call to call expression at: {}", expr->getBeginLoc().printToString(source_manager())); return true; @@ -1050,6 +1137,7 @@ bool translation_unit_visitor::VisitCallExpr(clang::CallExpr *expr) } } + // Add message to diagram if (m.from() > 0 && m.to() > 0) { if (!generated_message_from_comment) { auto expr_comment = get_expression_comment(source_manager(), @@ -1075,6 +1163,23 @@ bool translation_unit_visitor::VisitCallExpr(clang::CallExpr *expr) return true; } +bool translation_unit_visitor::generate_message_from_comment( + model::message &m) const +{ + auto generated_message_from_comment{false}; + for (const auto &decorator : m.decorators()) { + auto call_decorator = + std::dynamic_pointer_cast(decorator); + if (call_decorator && + call_decorator->applies_to_diagram(config().name)) { + m.set_to(common::to_id(call_decorator->callee)); + generated_message_from_comment = true; + break; + } + } + return generated_message_from_comment; +} + bool translation_unit_visitor::TraverseVarDecl(clang::VarDecl *decl) { if (decl->isStaticLocal()) @@ -1155,12 +1260,30 @@ bool translation_unit_visitor::process_operator_call_expression( operator_call_expr->getCalleeDecl()->getID(), operator_call_expr->getBeginLoc().printToString(source_manager())); - auto maybe_id = get_unique_id(operator_call_expr->getCalleeDecl()->getID()); - if (maybe_id.has_value()) { - m.set_to(maybe_id.value()); + // Handle the case if the callee is a lambda + if (const auto *lambda_method = clang::dyn_cast( + operator_call_expr->getCalleeDecl()); + lambda_method != nullptr && lambda_method->getParent()->isLambda()) { + + LOG_DBG("Operator callee is a lambda: {}", + common::to_string(lambda_method)); + + const auto source_location{ + lambda_source_location(lambda_method->getParent()->getLocation())}; + + auto lambda_name = make_lambda_name(lambda_method->getParent()); + + m.set_to(lambda_method->getParent()->getID()); } else { - m.set_to(operator_call_expr->getCalleeDecl()->getID()); + auto maybe_id = + get_unique_id(operator_call_expr->getCalleeDecl()->getID()); + if (maybe_id.has_value()) { + m.set_to(maybe_id.value()); + } + else { + m.set_to(operator_call_expr->getCalleeDecl()->getID()); + } } m.set_message_name(fmt::format( @@ -1513,9 +1636,6 @@ translation_unit_visitor::create_class_model(clang::CXXRecordDecl *cls) c.set_name(type_name); c.set_namespace(ns); c.set_id(common::to_id(c.full_name(false))); - - // TODO: Check if lambda is declared as an argument passed to a - // function/method call } else { LOG_WARN("Cannot find parent declaration for lambda {}", @@ -1675,29 +1795,38 @@ std::string translation_unit_visitor::simplify_system_template( return config().simplify_template_type(full_name); } +std::string translation_unit_visitor::lambda_source_location( + const clang::SourceLocation &source_location) const +{ + const auto file_line = + source_manager().getSpellingLineNumber(source_location); + const auto file_column = + source_manager().getSpellingColumnNumber(source_location); + const std::string file_name = + config() + .make_path_relative( + source_manager().getFilename(source_location).str()) + .string(); + return fmt::format("{}:{}:{}", file_name, file_line, file_column); +} + std::string translation_unit_visitor::make_lambda_name( const clang::CXXRecordDecl *cls) const { std::string result; const auto location = cls->getLocation(); - const auto file_line = source_manager().getSpellingLineNumber(location); - const auto file_column = source_manager().getSpellingColumnNumber(location); - const std::string file_name = - config() - .make_path_relative(source_manager().getFilename(location).str()) - .string(); + const std::string source_location{lambda_source_location(location)}; if (context().caller_id() != 0 && get_participant(context().caller_id()).has_value()) { auto parent_full_name = get_participant(context().caller_id()).value().full_name_no_ns(); - result = fmt::format("{}##(lambda {}:{}:{})", parent_full_name, - file_name, file_line, file_column); + result = + fmt::format("{}##(lambda {})", parent_full_name, source_location); } else { - result = - fmt::format("(lambda {}:{}:{})", file_name, file_line, file_column); + result = fmt::format("(lambda {})", source_location); } return result; @@ -1775,6 +1904,25 @@ void translation_unit_visitor::finalize() } } } + + // Change all messages with target set to an id of a lambda expression to + // to the ID of their operator() - this is necessary, as some calls to + // lambda expressions are visited before the actual lambda expressions + // are visited... + for (auto &[id, activity] : diagram().sequences()) { + for (auto &m : activity.messages()) { + auto participant = diagram().get_participant(m.to()); + + if (participant && participant.value().is_lambda() && + participant.value().lambda_operator_id() != 0) { + LOG_DBG("Changing lambda expression target id from {} to {}", + m.to(), participant.value().lambda_operator_id()); + + m.set_to(participant.value().lambda_operator_id()); + m.set_message_name("operator()"); + } + } + } } std::unique_ptr @@ -1882,7 +2030,21 @@ bool translation_unit_visitor::should_include(const clang::CallExpr *expr) const const auto expr_file = expr->getBeginLoc().printToString(source_manager()); - return diagram().should_include(common::model::source_file{expr_file}); + if (!diagram().should_include(common::model::source_file{expr_file})) + return false; + + const auto *callee_decl = expr->getCalleeDecl(); + + if (callee_decl != nullptr) { + const auto *callee_function = callee_decl->getAsFunction(); + + if ((callee_function == nullptr) || !should_include(callee_function)) + return false; + + return should_include(callee_function); + } + + return true; } bool translation_unit_visitor::should_include( diff --git a/src/sequence_diagram/visitor/translation_unit_visitor.h b/src/sequence_diagram/visitor/translation_unit_visitor.h index 11e4aae3..2c3cebb4 100644 --- a/src/sequence_diagram/visitor/translation_unit_visitor.h +++ b/src/sequence_diagram/visitor/translation_unit_visitor.h @@ -95,6 +95,8 @@ public: bool TraverseLambdaExpr(clang::LambdaExpr *expr); + bool TraverseCXXMethodDecl(clang::CXXMethodDecl *declaration); + bool VisitCXXMethodDecl(clang::CXXMethodDecl *declaration); bool VisitCXXRecordDecl(clang::CXXRecordDecl *declaration); @@ -104,6 +106,8 @@ public: bool VisitClassTemplateSpecializationDecl( clang::ClassTemplateSpecializationDecl *declaration); + bool TraverseFunctionDecl(clang::FunctionDecl *declaration); + bool VisitFunctionDecl(clang::FunctionDecl *declaration); bool VisitFunctionTemplateDecl( @@ -336,6 +340,21 @@ private: */ std::string make_lambda_name(const clang::CXXRecordDecl *cls) const; + /** + * @brief Render lambda source location to string + * + * Returns exact source code location of the lambda expression in the form + * ::. + * + * The filepath is relative to the `relative_to` config option. + * + * @param source_location Clang SourceLocation instance associated with + * lambda expression + * @return String representation of the location + */ + std::string lambda_source_location( + const clang::SourceLocation &source_location) const; + /** * @brief Check if template is a smart pointer * @@ -441,6 +460,15 @@ private: const clang::SourceManager &sm, const clang::ASTContext &context, int64_t caller_id, const clang::Stmt *stmt); + /** + * @brief Initializes model message from comment call directive + * + * @param m Message instance + * @return True, if the comment associated with the call expression + * contained a call directive and it was parsed correctly. + */ + bool generate_message_from_comment(model::message &m) const; + /** * @brief Get template builder reference * diff --git a/tests/t20044/.clang-uml b/tests/t20044/.clang-uml new file mode 100644 index 00000000..37ea1a93 --- /dev/null +++ b/tests/t20044/.clang-uml @@ -0,0 +1,17 @@ +add_compile_flags: + - -fparse-all-comments +diagrams: + t20044_sequence: + type: sequence + glob: + - t20044.cc + generate_message_comments: true + include: + namespaces: + - clanguml::t20044 + exclude: + namespaces: + - clanguml::t20044::detail2 + using_namespace: clanguml::t20044 + from: + - function: "clanguml::t20044::tmain()" \ No newline at end of file diff --git a/tests/t20044/t20044.cc b/tests/t20044/t20044.cc new file mode 100644 index 00000000..832e6c4b --- /dev/null +++ b/tests/t20044/t20044.cc @@ -0,0 +1,100 @@ +// #include "include/expected.hpp" + +#include +#include +#include + +namespace clanguml { +namespace t20044 { + +enum class error { OK, FAIL }; + +namespace detail { +// Trivial std::expected mock-up just for testing calls through lambda +// expressions passed as arguments to methods +template class expected { +private: + std::optional value_; + std::optional error_; + +public: + explicit expected(V v) + : value_{std::move(v)} + { + } + explicit expected(E e) + : error_{std::move(e)} + { + } + + const auto &value() const { return *value_; } + + const auto &error() const { return *error_; } + + template auto and_then(F &&f) && + { + if (value_) + return f(*value_); + + return *this; + } +}; +} // namespace detail + +namespace detail2 { +template void run(F &&f) { f(); } +} // namespace detail2 + +using result_t = detail::expected; + +struct A { + auto a() const { } + + auto a1() const { return result_t{10}; } + + auto a2(int arg) const { return result_t{arg + 1}; } + + auto a4(int arg) const { return result_t{arg + 1}; } + + void a5() { } +}; + +auto a3(int arg) { return result_t{arg + 1}; } + +struct R { + template R(F &&f) { f(); } +}; + +int tmain() +{ + A a; + + // Call to template constructor with callable parameter and lambda + // expression as argument + R r([&a]() { a.a(); }); + + std::function a4_wrapper = &A::a4; + + std::function a4_wrapper_to_a = + std::bind(a4_wrapper, a, std::placeholders::_1); + + // The message to detail2::run() is skipped due to exclude filter, however + // the call to lambda and A::a5() is rendered + // TODO: Add some marker to highlight that this is not a direct call + detail2::run([&]() { a.a5(); }); + + return a + .a1() + // Call to a template method accepting a callable with lambda expression + // as argument, fully tracked showing method's activity and + .and_then([&](auto &&arg) { return a.a2(arg); }) + // TODO: Call to a method accepting a callable with function pointer + // as argument + .and_then(a3) + // TODO: Call to a method accepting a callable with std::function as + // argument + .and_then(a4_wrapper_to_a) + .value(); +} +} +} \ No newline at end of file diff --git a/tests/t20044/test_case.h b/tests/t20044/test_case.h new file mode 100644 index 00000000..b7bbadb4 --- /dev/null +++ b/tests/t20044/test_case.h @@ -0,0 +1,91 @@ +/** + * tests/t20044/test_case.h + * + * Copyright (c) 2021-2024 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("t20044", "[test-case][sequence]") +{ + auto [config, db] = load_config("t20044"); + + auto diagram = config.diagrams["t20044_sequence"]; + + REQUIRE(diagram->name == "t20044_sequence"); + + auto model = generate_sequence_diagram(*db, diagram); + + REQUIRE(model->name() == "t20044_sequence"); + + { + auto src = generate_sequence_puml(diagram, *model); + AliasMatcher _A(src); + + REQUIRE_THAT(src, StartsWith("@startuml")); + REQUIRE_THAT(src, EndsWith("@enduml\n")); + + // Check if all calls exist + REQUIRE_THAT(src, + HasCall( + _A("tmain()"), _A("R"), "R((lambda at t20044.cc:74:9) &&)")); + REQUIRE_THAT(src, + HasCall(_A("R"), _A("tmain()::(lambda t20044.cc:74:9)"), + "operator()()")); + REQUIRE_THAT(src, + HasCall(_A("tmain()::(lambda t20044.cc:74:9)"), _A("A"), "a()")); + + REQUIRE_THAT(src, + HasCall(_A("tmain()"), _A("tmain()::(lambda t20044.cc:84:18)"), + "operator()()")); + REQUIRE_THAT(src, + HasCall(_A("tmain()::(lambda t20044.cc:84:18)"), _A("A"), "a5()")); + + REQUIRE_THAT(src, HasCall(_A("tmain()"), _A("A"), "a1()")); + + REQUIRE_THAT(src, + HasCall(_A("tmain()"), _A("detail::expected"), + "and_then((lambda at t20044.cc:90:19) &&)")); + + REQUIRE_THAT(src, + HasCall(_A("detail::expected"), + _A("tmain()::(lambda t20044.cc:90:19)"), "operator()()")); + + REQUIRE_THAT(src, + HasCall( + _A("tmain()::(lambda t20044.cc:90:19)"), _A("A"), "a2(int)")); + + REQUIRE_THAT(src, + HasCall( + _A("A"), _A("detail::expected"), "expected(int)")); + + save_puml(config.output_directory(), diagram->name + ".puml", src); + } + + { + auto j = generate_sequence_json(diagram, *model); + + using namespace json; + + save_json(config.output_directory(), diagram->name + ".json", j); + } + + { + auto src = generate_sequence_mermaid(diagram, *model); + + mermaid::AliasMatcher _A(src); + using mermaid::IsClass; + + 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 3f92a7fc..8dc2598b 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -470,6 +470,7 @@ using namespace clanguml::test::matchers; #include "t20041/test_case.h" #include "t20042/test_case.h" #include "t20043/test_case.h" +#include "t20044/test_case.h" /// /// Package diagram tests diff --git a/uml/class/class_translation_unit_visitor.yml b/uml/class/class_translation_unit_visitor.yml index 2180a56b..ca9a7f43 100644 --- a/uml/class/class_translation_unit_visitor.yml +++ b/uml/class/class_translation_unit_visitor.yml @@ -2,7 +2,7 @@ type: class title: Class diagram TU visitor include_relations_also_as_members: false generate_method_arguments: none -generate_packages: false +generate_packages: true glob: - src/common/visitor/*.cc - src/class_diagram/visitor/*.cc