Added option to enable rendering return types in sequence diagrams (fixes #93)

This commit is contained in:
Bartek Kryza
2023-07-04 00:38:42 +02:00
parent d944a2cead
commit 2104d930a8
19 changed files with 274 additions and 29 deletions

View File

@@ -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)
<!-- tocstop -->
@@ -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.

View File

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

View File

@@ -445,6 +445,7 @@ struct inheritable_diagram_options {
"comment_parser", comment_parser_t::plain};
option<bool> combine_free_functions_into_file_participants{
"combine_free_functions_into_file_participants", false};
option<bool> generate_return_types{"generate_return_types", false};
option<std::vector<std::string>> participants_order{"participants_order"};
option<bool> debug_mode{"debug_mode", false};
option<bool> generate_metadata{"generate_metadata", true};

View File

@@ -513,6 +513,7 @@ template <> struct convert<git_config> {
template <typename T> 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<sequence_diagram> {
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<config> {
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<std::string>());
get_option(node, rhs.relative_to);

View File

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

View File

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

View File

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

View File

@@ -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<sequence_diagram::model::function>(
m.to());
if (to_participant.has_value()) {
m.set_return_type(to_participant.value().return_type());
}
}
block_message_stack.back().push_back(m);
}
}

View File

@@ -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<std::string> &function::parameters() const

View File

@@ -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<std::string> parameters_;
};

View File

@@ -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<model::function> 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(

15
tests/t20032/.clang-uml Normal file
View File

@@ -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 **)"

27
tests/t20032/t20032.cc Normal file
View File

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

79
tests/t20032/test_case.h Normal file
View File

@@ -0,0 +1,79 @@
/**
* tests/t20032/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("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<int> 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);
}
}

View File

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

View File

@@ -134,12 +134,15 @@ struct HasCallWithResultMatcher : ContainsMatcher {
template <typename T> class HasCallMatcher : public Catch::MatcherBase<T> {
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<std::string> 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<std::string> 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<std::string> 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

View File

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

View File

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

View File

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