diff --git a/docs/sequence_diagrams.md b/docs/sequence_diagrams.md index f1c802df..31b484d0 100644 --- a/docs/sequence_diagrams.md +++ b/docs/sequence_diagrams.md @@ -7,6 +7,7 @@ * [Grouping free functions by file](#grouping-free-functions-by-file) * [Lambda expressions in sequence diagrams](#lambda-expressions-in-sequence-diagrams) * [Customizing participants order](#customizing-participants-order) +* [Generating return types](#generating-return-types) @@ -247,3 +248,18 @@ diagrams: - clanguml::t20029::encode_b64(std::string &&) ``` +## Generating return types +By default, return messages do not contain the return type information from +the function or method. Instead, if the result is void there is no return +arrow from the activity representing the function body. + +It is however possible to enable rendering of return types, by adding the +following configuration option: + +```yaml +generate_return_types: true +``` + +This option only affects the `plantuml` generation, in `json` generator +`return_type` property is always present in the message nodes. + diff --git a/src/config/config.cc b/src/config/config.cc index e4273c00..2dab8e2b 100644 --- a/src/config/config.cc +++ b/src/config/config.cc @@ -166,7 +166,8 @@ void inheritable_diagram_options::inherit( relative_to.override(parent.relative_to); comment_parser.override(parent.comment_parser); combine_free_functions_into_file_participants.override( - combine_free_functions_into_file_participants); + parent.combine_free_functions_into_file_participants); + generate_return_types.override(parent.generate_return_types); debug_mode.override(parent.debug_mode); generate_metadata.override(parent.generate_metadata); } diff --git a/src/config/config.h b/src/config/config.h index 4bde1ddb..526f02bc 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -445,6 +445,7 @@ struct inheritable_diagram_options { "comment_parser", comment_parser_t::plain}; option combine_free_functions_into_file_participants{ "combine_free_functions_into_file_participants", false}; + option generate_return_types{"generate_return_types", false}; option> participants_order{"participants_order"}; option debug_mode{"debug_mode", false}; option generate_metadata{"generate_metadata", true}; diff --git a/src/config/yaml_decoders.cc b/src/config/yaml_decoders.cc index 5bfdbe9c..25068b0f 100644 --- a/src/config/yaml_decoders.cc +++ b/src/config/yaml_decoders.cc @@ -513,6 +513,7 @@ template <> struct convert { template bool decode_diagram(const Node &node, T &rhs) { + // Decode options common for all diagrams get_option(node, rhs.glob); get_option(node, rhs.using_namespace); get_option(node, rhs.include); @@ -566,6 +567,7 @@ template <> struct convert { get_option(node, rhs.start_from); get_option(node, rhs.combine_free_functions_into_file_participants); + get_option(node, rhs.generate_return_types); get_option(node, rhs.relative_to); get_option(node, rhs.participants_order); get_option(node, rhs.generate_method_arguments); @@ -752,6 +754,9 @@ template <> struct convert { get_option(node, rhs.comment_parser); get_option(node, rhs.debug_mode); get_option(node, rhs.generate_metadata); + get_option(node, rhs.combine_free_functions_into_file_participants); + get_option(node, rhs.generate_return_types); + rhs.base_directory.set(node["__parent_path"].as()); get_option(node, rhs.relative_to); diff --git a/src/config/yaml_emitters.cc b/src/config/yaml_emitters.cc index 8b629290..83f0d20f 100644 --- a/src/config/yaml_emitters.cc +++ b/src/config/yaml_emitters.cc @@ -289,6 +289,7 @@ YAML::Emitter &operator<<( } out << c.comment_parser; out << c.combine_free_functions_into_file_participants; + out << c.generate_return_types; out << c.participants_order; out << c.debug_mode; diff --git a/src/sequence_diagram/generators/json/sequence_diagram_generator.cc b/src/sequence_diagram/generators/json/sequence_diagram_generator.cc index 33f298cb..7f887a39 100644 --- a/src/sequence_diagram/generators/json/sequence_diagram_generator.cc +++ b/src/sequence_diagram/generators/json/sequence_diagram_generator.cc @@ -645,16 +645,15 @@ void generator::generate(std::ostream &ostr) const generate_activity( m_model.get_activity(start_from), visited_participants); - json_["sequences"].push_back(std::move(sequence)); - block_statements_stack_.pop_back(); if (from.value().type_name() == "method" || m_config.combine_free_functions_into_file_participants()) { - // TODO - // sequence["return_type"] = from.value().id() + sequence["return_type"] = from.value().return_type(); } + + json_["sequences"].push_back(std::move(sequence)); } else { // TODO: Add support for other sequence start location types diff --git a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc index 03b31fb4..1fb1d253 100644 --- a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc +++ b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc @@ -128,7 +128,13 @@ void generator::generate_return(const message &m, std::ostream &ostr) const ostr << to_alias << " " << common::generators::plantuml::to_plantuml(message_t::kReturn) - << " " << from_alias << '\n'; + << " " << from_alias; + + if (m_config.generate_return_types()) { + ostr << " : //" << m.return_type() << "//"; + } + + ostr << '\n'; } } @@ -451,7 +457,12 @@ void generator::generate(std::ostream &ostr) const if (!from.value().is_void()) { ostr << "[<--" - << " " << from_alias << std::endl; + << " " << from_alias; + + if (m_config.generate_return_types()) + ostr << " : //" << from.value().return_type() << "//"; + + ostr << '\n'; } } diff --git a/src/sequence_diagram/model/diagram.cc b/src/sequence_diagram/model/diagram.cc index 9cb1ed81..365adda7 100644 --- a/src/sequence_diagram/model/diagram.cc +++ b/src/sequence_diagram/model/diagram.cc @@ -312,6 +312,16 @@ void diagram::finalize() assert(block_nest_level >= 0); } else { + if (m.type() == message_t::kCall) { + // Set the message return type based on the callee return + // type + auto to_participant = + get_participant( + m.to()); + if (to_participant.has_value()) { + m.set_return_type(to_participant.value().return_type()); + } + } block_message_stack.back().push_back(m); } } diff --git a/src/sequence_diagram/model/participant.cc b/src/sequence_diagram/model/participant.cc index 3772b891..5c8f15c8 100644 --- a/src/sequence_diagram/model/participant.cc +++ b/src/sequence_diagram/model/participant.cc @@ -147,6 +147,10 @@ bool function::is_operator() const { return is_operator_; } void function::is_operator(bool o) { is_operator_ = o; } +void function::return_type(const std::string &rt) { return_type_ = rt; } + +const std::string &function::return_type() const { return return_type_; } + void function::add_parameter(const std::string &a) { parameters_.push_back(a); } const std::vector &function::parameters() const diff --git a/src/sequence_diagram/model/participant.h b/src/sequence_diagram/model/participant.h index 7cf80cd1..d6f014a3 100644 --- a/src/sequence_diagram/model/participant.h +++ b/src/sequence_diagram/model/participant.h @@ -291,6 +291,20 @@ struct function : public participant { */ void is_operator(bool o); + /** + * @brief Set functions return type + * + * @param rt Return type + */ + void return_type(const std::string &rt); + + /** + * @brief Get function return type + * + * @return Return type + */ + const std::string &return_type() const; + /** * @brief Add a function parameter * @@ -313,6 +327,7 @@ private: bool is_void_{false}; bool is_static_{false}; bool is_operator_{false}; + std::string return_type_; std::vector parameters_; }; diff --git a/src/sequence_diagram/visitor/translation_unit_visitor.cc b/src/sequence_diagram/visitor/translation_unit_visitor.cc index c295bfea..273b8fa8 100644 --- a/src/sequence_diagram/visitor/translation_unit_visitor.cc +++ b/src/sequence_diagram/visitor/translation_unit_visitor.cc @@ -972,13 +972,6 @@ bool translation_unit_visitor::VisitCallExpr(clang::CallExpr *expr) } } - // - // This crashes on LLVM <= 12, for now just return empty type - // - // const auto &return_type = - // function_call_expr->getCallReturnType(current_ast_context); - // m.return_type = return_type.getAsString(); - if (m.from() > 0 && m.to() > 0) { if (diagram().sequences().find(m.from()) == diagram().sequences().end()) { @@ -1550,6 +1543,10 @@ translation_unit_visitor::build_function_template( process_template_parameters(declaration, *function_template_model_ptr); + function_template_model_ptr->return_type( + common::to_string(declaration.getAsFunction()->getReturnType(), + declaration.getASTContext())); + for (const auto *param : declaration.getTemplatedDecl()->parameters()) { function_template_model_ptr->add_parameter( simplify_system_template(common::to_string( @@ -1612,6 +1609,9 @@ std::unique_ptr translation_unit_visitor::build_function_model( ns.pop_back(); function_model_ptr->set_namespace(ns); + function_model_ptr->return_type(common::to_string( + declaration.getReturnType(), declaration.getASTContext())); + for (const auto *param : declaration.parameters()) { function_model_ptr->add_parameter( simplify_system_template(common::to_string( @@ -2337,6 +2337,9 @@ translation_unit_visitor::create_method_model(clang::CXXMethodDecl *declaration) "::" + declaration->getNameAsString()); method_model_ptr->is_static(declaration->isStatic()); + method_model_ptr->return_type(common::to_string( + declaration->getReturnType(), declaration->getASTContext())); + for (const auto *param : declaration->parameters()) { method_model_ptr->add_parameter(config().using_namespace().relative( simplify_system_template(common::to_string( diff --git a/tests/t20032/.clang-uml b/tests/t20032/.clang-uml new file mode 100644 index 00000000..48ccf1d2 --- /dev/null +++ b/tests/t20032/.clang-uml @@ -0,0 +1,15 @@ +compilation_database_dir: .. +output_directory: puml +diagrams: + t20032_sequence: + type: sequence + glob: + - ../../tests/t20032/t20032.cc + include: + namespaces: + - clanguml::t20032 + using_namespace: + - clanguml::t20032 + generate_return_types: true + start_from: + - function: "clanguml::t20032::tmain(int,char **)" \ No newline at end of file diff --git a/tests/t20032/t20032.cc b/tests/t20032/t20032.cc new file mode 100644 index 00000000..a87aac81 --- /dev/null +++ b/tests/t20032/t20032.cc @@ -0,0 +1,27 @@ +namespace clanguml { +namespace t20032 { + +struct A { + int a1(int i) { return i; } + double a2(double d) { return d; } + const char *a3(const char *s) { return s; } +}; + +struct B { + int b(int i) { return a.a1(i); } + double b(double d) { return a.a2(d); } + const char *b(const char *s) { return a.a3(s); } + + A a; +}; + +void tmain(int argc, char **argv) +{ + B b; + + b.b(1); + b.b(2.0); + b.b("three"); +} +} +} \ No newline at end of file diff --git a/tests/t20032/test_case.h b/tests/t20032/test_case.h new file mode 100644 index 00000000..d693533a --- /dev/null +++ b/tests/t20032/test_case.h @@ -0,0 +1,79 @@ +/** + * tests/t20032/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("t20032", "[test-case][sequence]") +{ + auto [config, db] = load_config("t20032"); + + auto diagram = config.diagrams["t20032_sequence"]; + + REQUIRE(diagram->name == "t20032_sequence"); + + auto model = generate_sequence_diagram(*db, diagram); + + REQUIRE(model->name() == "t20032_sequence"); + + REQUIRE(model->name() == "t20032_sequence"); + { + auto puml = generate_sequence_puml(diagram, *model); + AliasMatcher _A(puml); + + REQUIRE_THAT(puml, StartsWith("@startuml")); + REQUIRE_THAT(puml, EndsWith("@enduml\n")); + + // Check if all calls exist + REQUIRE_THAT( + puml, HasCall(_A("tmain(int,char **)"), _A("B"), "b(int)")); + REQUIRE_THAT( + puml, HasResponse(_A("tmain(int,char **)"), _A("B"), "int")); + + REQUIRE_THAT(puml, HasCall(_A("B"), _A("A"), "a1(int)")); + REQUIRE_THAT(puml, HasResponse(_A("B"), _A("A"), "int")); + + REQUIRE_THAT( + puml, HasCall(_A("tmain(int,char **)"), _A("B"), "b(double)")); + REQUIRE_THAT(puml, HasCall(_A("B"), _A("A"), "a2(double)")); + REQUIRE_THAT(puml, HasResponse(_A("B"), _A("A"), "double")); + + REQUIRE_THAT(puml, + HasCall(_A("tmain(int,char **)"), _A("B"), "b(const char *)")); + REQUIRE_THAT(puml, HasCall(_A("B"), _A("A"), "a3(const char *)")); + REQUIRE_THAT(puml, HasResponse(_A("B"), _A("A"), "const char *")); + + save_puml( + config.output_directory() + "/" + diagram->name + ".puml", puml); + } + + { + auto j = generate_sequence_json(diagram, *model); + + using namespace json; + + std::vector messages = { + FindMessage(j, "tmain(int,char **)", "B", "b(int)", "int"), + FindMessage(j, "B", "A", "a1(int)", "int"), + FindMessage(j, "tmain(int,char **)", "B", "b(double)"), + FindMessage(j, "B", "A", "a2(double)"), + FindMessage(j, "tmain(int,char **)", "B", "b(const char *)"), + FindMessage(j, "B", "A", "a3(const char *)")}; + + REQUIRE(std::is_sorted(messages.begin(), messages.end())); + + save_json(config.output_directory() + "/" + diagram->name + ".json", j); + } +} \ No newline at end of file diff --git a/tests/test_cases.cc b/tests/test_cases.cc index 48ce7052..7171278e 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -345,6 +345,7 @@ using namespace clanguml::test::matchers; #include "t20029/test_case.h" #include "t20030/test_case.h" #include "t20031/test_case.h" +#include "t20032/test_case.h" /// /// Package diagram tests diff --git a/tests/test_cases.h b/tests/test_cases.h index 33e820c6..df5c789d 100644 --- a/tests/test_cases.h +++ b/tests/test_cases.h @@ -134,12 +134,15 @@ struct HasCallWithResultMatcher : ContainsMatcher { template class HasCallMatcher : public Catch::MatcherBase { T m_from, m_to, m_message; + bool m_is_response; + std::string call_pattern, response_pattern; public: - HasCallMatcher(T from, T to, T message) + HasCallMatcher(T from, T to, T message, bool is_response = false) : m_from(from) , m_to{to} , m_message{message} + , m_is_response{is_response} { util::replace_all(m_message, "(", "\\("); util::replace_all(m_message, ")", "\\)"); @@ -147,15 +150,22 @@ public: util::replace_all(m_message, "[", "\\["); util::replace_all(m_message, "]", "\\]"); util::replace_all(m_message, "+", "\\+"); + + call_pattern = fmt::format("{} -> {} " + "(\\[\\[.*\\]\\] )?: {}", + m_from, m_to, m_message); + + response_pattern = + fmt::format("{} --> {} : //{}//", m_from, m_to, m_message); } bool match(T const &in) const override { std::istringstream fin(in); std::string line; - std::regex r{fmt::format("{} -> {} " - "(\\[\\[.*\\]\\] )?: {}", - m_from, m_to, m_message)}; + + std::regex r{m_is_response ? response_pattern : call_pattern}; + while (std::getline(fin, line)) { std::smatch base_match; std::regex_search(in, base_match, r); @@ -182,6 +192,13 @@ auto HasCall(std::string const &from, std::string const &to, return HasCallMatcher(from, to, message); } +auto HasResponse(std::string const &from, std::string const &to, + std::string const &message, + CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes) +{ + return HasCallMatcher(to, from, message, true); +} + auto HasCallInControlCondition(std::string const &from, std::string const &to, std::string const &message, CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes) @@ -955,7 +972,8 @@ bool IsFileParticipant(const nlohmann::json &j, const std::string &name) namespace detail { int find_message_nested(const nlohmann::json &j, const std::string &from, - const std::string &to, const std::string &msg, const nlohmann::json &from_p, + const std::string &to, const std::string &msg, + std::optional return_type, const nlohmann::json &from_p, const nlohmann::json &to_p, int &count) { const auto &messages = j["messages"]; @@ -965,23 +983,25 @@ int find_message_nested(const nlohmann::json &j, const std::string &from, for (const auto &m : messages) { if (m.contains("branches")) { for (const auto &b : m["branches"]) { - auto nested_res = - find_message_nested(b, from, to, msg, from_p, to_p, count); + auto nested_res = find_message_nested( + b, from, to, msg, return_type, from_p, to_p, count); if (nested_res >= 0) return nested_res; } } else if (m.contains("messages")) { - auto nested_res = - find_message_nested(m, from, to, msg, from_p, to_p, count); + auto nested_res = find_message_nested( + m, from, to, msg, return_type, from_p, to_p, count); if (nested_res >= 0) return nested_res; } else { if ((m["from"]["participant_id"] == from_p["id"]) && - (m["to"]["participant_id"] == to_p["id"]) && (m["name"] == msg)) + (m["to"]["participant_id"] == to_p["id"]) && + (m["name"] == msg) && + (!return_type || m["return_type"] == *return_type)) return count; count++; @@ -992,7 +1012,8 @@ int find_message_nested(const nlohmann::json &j, const std::string &from, } int find_message_impl(const nlohmann::json &j, const std::string &from, - const std::string &to, const std::string &msg) + const std::string &to, const std::string &msg, + std::optional return_type) { auto from_p = get_participant(j, from); @@ -1004,7 +1025,7 @@ int find_message_impl(const nlohmann::json &j, const std::string &from, int count{0}; auto res = detail::find_message_nested( - sequence_0, from, to, msg, *from_p, *to_p, count); + sequence_0, from, to, msg, return_type, *from_p, *to_p, count); if (res >= 0) return res; @@ -1018,14 +1039,15 @@ int find_message_impl(const nlohmann::json &j, const std::string &from, int FindMessage(const nlohmann::json &j, const File &from, const File &to, const std::string &msg) { - return detail::find_message_impl(j, from.file, to.file, msg); + return detail::find_message_impl(j, from.file, to.file, msg, {}); } int FindMessage(const nlohmann::json &j, const std::string &from, - const std::string &to, const std::string &msg) + const std::string &to, const std::string &msg, + std::optional return_type = {}) { return detail::find_message_impl( - j, expand_name(j, from), expand_name(j, to), msg); + j, expand_name(j, from), expand_name(j, to), msg, return_type); } } // namespace json diff --git a/tests/test_cases.yaml b/tests/test_cases.yaml index 63415069..d98bf626 100644 --- a/tests/test_cases.yaml +++ b/tests/test_cases.yaml @@ -292,6 +292,9 @@ test_cases: - name: t20031 title: Callee type sequence diagram filter test case description: + - name: t20032 + title: Return type generation option sequence diagram test case + description: Package diagrams: - name: t30001 title: Basic package diagram test case diff --git a/tests/test_config.cc b/tests/test_config.cc index 486c1619..c16970fa 100644 --- a/tests/test_config.cc +++ b/tests/test_config.cc @@ -323,3 +323,20 @@ TEST_CASE("Test config diagram_templates", "[unit-test]") REQUIRE(cfg.diagram_templates()["main_sequence_tmpl"].type == clanguml::common::model::diagram_t::kSequence); } + +TEST_CASE("Test config sequence inherited", "[unit-test]") +{ + auto cfg = clanguml::config::load( + "./test_config_data/sequence_inheritable_options.yml"); + + CHECK(cfg.diagrams.size() == 2); + auto &def = *cfg.diagrams["sequence_default"]; + CHECK(def.type() == clanguml::common::model::diagram_t::kSequence); + CHECK(def.combine_free_functions_into_file_participants() == true); + CHECK(def.generate_return_types() == true); + + def = *cfg.diagrams["sequence_default2"]; + CHECK(def.type() == clanguml::common::model::diagram_t::kSequence); + CHECK(def.combine_free_functions_into_file_participants() == false); + CHECK(def.generate_return_types() == false); +} \ No newline at end of file diff --git a/tests/test_config_data/sequence_inheritable_options.yml b/tests/test_config_data/sequence_inheritable_options.yml new file mode 100644 index 00000000..0187c099 --- /dev/null +++ b/tests/test_config_data/sequence_inheritable_options.yml @@ -0,0 +1,15 @@ +compilation_database_dir: debug +output_directory: output +combine_free_functions_into_file_participants: true +generate_return_types: true +diagrams: + sequence_default: + type: sequence + glob: + - src/*.cc + sequence_default2: + type: sequence + glob: + - src/*.cc + combine_free_functions_into_file_participants: false + generate_return_types: false