Added option to include if and loop condition text in the diagram (fixes #162)

This commit is contained in:
Bartek Kryza
2023-07-04 23:58:42 +02:00
parent a514532e51
commit 3bd8f7f7a8
20 changed files with 365 additions and 13 deletions

View File

@@ -8,6 +8,7 @@
* [Lambda expressions in sequence diagrams](#lambda-expressions-in-sequence-diagrams)
* [Customizing participants order](#customizing-participants-order)
* [Generating return types](#generating-return-types)
* [Generating condition statements](#generating-condition-statements)
<!-- tocstop -->
@@ -263,3 +264,15 @@ generate_return_types: true
This option only affects the `plantuml` generation, in `json` generator
`return_type` property is always present in the message nodes.
## Generating condition statements
Sometimes, it is useful to include actual condition statements (for instance
contents of the `if()` condition in the `alt` or `loop` blocks in the sequence
diagrams, to make them more readable.
This can be enabled using the following option:
```yaml
generate_condition_statements: true
```

View File

@@ -564,6 +564,75 @@ bool is_type_token(const std::string &t)
(is_identifier(t) && !is_qualifier(t) && !is_bracket(t));
}
std::string format_condition_text(const std::string &condition_text)
{
std::string result{condition_text};
if (result.size() < 2)
return {};
std::vector<std::string> text_lines = util::split(result, "\n", true);
// Trim each line
for (auto &line : text_lines) {
line = util::trim(line);
}
result = util::join(" ", text_lines);
if (result.at(0) == '(' && result.back() == ')')
return result.substr(1, result.size() - 2);
return result;
}
std::string get_condition_text(clang::SourceManager &sm, clang::IfStmt *stmt)
{
auto condition_range =
clang::SourceRange(stmt->getLParenLoc(), stmt->getRParenLoc());
return format_condition_text(get_source_text(condition_range, sm));
}
std::string get_condition_text(clang::SourceManager &sm, clang::WhileStmt *stmt)
{
auto condition_range =
clang::SourceRange(stmt->getLParenLoc(), stmt->getRParenLoc());
return format_condition_text(get_source_text(condition_range, sm));
}
std::string get_condition_text(
clang::SourceManager &sm, clang::CXXForRangeStmt *stmt)
{
auto condition_range = stmt->getRangeStmt()->getSourceRange();
return format_condition_text(get_source_text(condition_range, sm));
}
std::string get_condition_text(clang::SourceManager &sm, clang::ForStmt *stmt)
{
auto condition_range =
clang::SourceRange(stmt->getLParenLoc(), stmt->getRParenLoc());
return format_condition_text(get_source_text(condition_range, sm));
}
std::string get_condition_text(clang::SourceManager &sm, clang::DoStmt *stmt)
{
auto condition_range = stmt->getCond()->getSourceRange();
return format_condition_text(get_source_text(condition_range, sm));
}
std::string get_condition_text(
clang::SourceManager &sm, clang::ConditionalOperator *stmt)
{
auto condition_range = stmt->getCond()->getSourceRange();
return format_condition_text(get_source_text(condition_range, sm));
}
clang::QualType dereference(clang::QualType type)
{
auto res = type;

View File

@@ -23,6 +23,7 @@
#include "types.h"
#include "util/util.h"
#include <clang/AST/Expr.h>
#include <clang/AST/RecursiveASTVisitor.h>
#include <deque>
@@ -248,6 +249,23 @@ bool is_qualified_identifier(const std::string &t);
bool is_type_token(const std::string &t);
std::string format_condition_text(const std::string &condition_text);
std::string get_condition_text(clang::SourceManager &sm, clang::IfStmt *stmt);
std::string get_condition_text(
clang::SourceManager &sm, clang::WhileStmt *stmt);
std::string get_condition_text(
clang::SourceManager &sm, clang::CXXForRangeStmt *stmt);
std::string get_condition_text(clang::SourceManager &sm, clang::ForStmt *stmt);
std::string get_condition_text(clang::SourceManager &sm, clang::DoStmt *stmt);
std::string get_condition_text(
clang::SourceManager &sm, clang::ConditionalOperator *stmt);
clang::QualType dereference(clang::QualType type);
/**

View File

@@ -168,6 +168,8 @@ void inheritable_diagram_options::inherit(
combine_free_functions_into_file_participants.override(
parent.combine_free_functions_into_file_participants);
generate_return_types.override(parent.generate_return_types);
generate_condition_statements.override(
parent.generate_condition_statements);
debug_mode.override(parent.debug_mode);
generate_metadata.override(parent.generate_metadata);
}

View File

@@ -446,6 +446,8 @@ struct inheritable_diagram_options {
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<bool> generate_condition_statements{
"generate_condition_statements", 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

@@ -568,6 +568,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.generate_condition_statements);
get_option(node, rhs.relative_to);
get_option(node, rhs.participants_order);
get_option(node, rhs.generate_method_arguments);
@@ -756,6 +757,7 @@ template <> struct convert<config> {
get_option(node, rhs.generate_metadata);
get_option(node, rhs.combine_free_functions_into_file_participants);
get_option(node, rhs.generate_return_types);
get_option(node, rhs.generate_condition_statements);
rhs.base_directory.set(node["__parent_path"].as<std::string>());
get_option(node, rhs.relative_to);

View File

@@ -290,6 +290,7 @@ YAML::Emitter &operator<<(
out << c.comment_parser;
out << c.combine_free_functions_into_file_participants;
out << c.generate_return_types;
out << c.generate_condition_statements;
out << c.participants_order;
out << c.debug_mode;

View File

@@ -231,7 +231,7 @@ void generator::generate_activity(const activity &a,
process_conditional_message(m);
break;
case message_t::kConditionalElse:
process_conditional_else_message();
process_conditional_else_message(m);
break;
case message_t::kConditionalEnd:
process_end_conditional_message();
@@ -279,6 +279,8 @@ void generator::process_while_message(const message &m) const
while_block["type"] = "loop";
while_block["name"] = "while";
while_block["activity_id"] = std::to_string(m.from());
if (auto text = m.condition_text(); text.has_value())
while_block["condition_text"] = *text;
current_block_statement()["messages"].push_back(std::move(while_block));
@@ -298,6 +300,8 @@ void generator::process_for_message(const message &m) const
for_block["type"] = "loop";
for_block["name"] = "for";
for_block["activity_id"] = std::to_string(m.from());
if (auto text = m.condition_text(); text.has_value())
for_block["condition_text"] = *text;
current_block_statement()["messages"].push_back(std::move(for_block));
@@ -317,6 +321,8 @@ void generator::process_do_message(const message &m) const
do_block["type"] = "loop";
do_block["name"] = "do";
do_block["activity_id"] = std::to_string(m.from());
if (auto text = m.condition_text(); text.has_value())
do_block["condition_text"] = *text;
current_block_statement()["messages"].push_back(std::move(do_block));
@@ -413,6 +419,8 @@ void generator::process_conditional_message(const message &m) const
if_block["type"] = "alt";
if_block["name"] = "conditional";
if_block["activity_id"] = std::to_string(m.from());
if (auto text = m.condition_text(); text.has_value())
if_block["condition_text"] = *text;
current_block_statement()["messages"].push_back(std::move(if_block));
@@ -427,13 +435,15 @@ void generator::process_conditional_message(const message &m) const
std::ref(current_block_statement()["branches"].back()));
}
void generator::process_conditional_else_message() const
void generator::process_conditional_else_message(const message &m) const
{
// remove previous branch from the stack
block_statements_stack_.pop_back();
nlohmann::json branch;
branch["type"] = "alternative";
if (auto text = m.condition_text(); text.has_value())
branch["condition_text"] = *text;
current_block_statement()["branches"].push_back(std::move(branch));
block_statements_stack_.push_back(
@@ -477,6 +487,8 @@ void generator::process_if_message(const message &m) const
if_block["type"] = "alt";
if_block["name"] = "if";
if_block["activity_id"] = std::to_string(m.from());
if (auto text = m.condition_text(); text.has_value())
if_block["condition_text"] = *text;
current_block_statement()["messages"].push_back(std::move(if_block));

View File

@@ -158,8 +158,10 @@ private:
/**
* @brief Process conditional else statement message
*
* @param m Message model
*/
void process_conditional_else_message() const;
void process_conditional_else_message(const model::message &m) const;
/**
* @brief Process `switch` statement message

View File

@@ -178,11 +178,17 @@ void generator::generate_activity(const activity &a, std::ostream &ostr,
}
else if (m.type() == message_t::kIf) {
print_debug(m, ostr);
ostr << "alt\n";
ostr << "alt";
if (m.condition_text())
ostr << " " << m.condition_text().value();
ostr << '\n';
}
else if (m.type() == message_t::kElseIf) {
print_debug(m, ostr);
ostr << "else\n";
ostr << "else";
if (m.condition_text())
ostr << " " << m.condition_text().value();
ostr << '\n';
}
else if (m.type() == message_t::kElse) {
print_debug(m, ostr);
@@ -193,21 +199,30 @@ void generator::generate_activity(const activity &a, std::ostream &ostr,
}
else if (m.type() == message_t::kWhile) {
print_debug(m, ostr);
ostr << "loop\n";
ostr << "loop";
if (m.condition_text())
ostr << " " << m.condition_text().value();
ostr << '\n';
}
else if (m.type() == message_t::kWhileEnd) {
ostr << "end\n";
}
else if (m.type() == message_t::kFor) {
print_debug(m, ostr);
ostr << "loop\n";
ostr << "loop";
if (m.condition_text())
ostr << " " << m.condition_text().value();
ostr << '\n';
}
else if (m.type() == message_t::kForEnd) {
ostr << "end\n";
}
else if (m.type() == message_t::kDo) {
print_debug(m, ostr);
ostr << "loop\n";
ostr << "loop";
if (m.condition_text())
ostr << " " << m.condition_text().value();
ostr << '\n';
}
else if (m.type() == message_t::kDoEnd) {
ostr << "end\n";
@@ -237,7 +252,10 @@ void generator::generate_activity(const activity &a, std::ostream &ostr,
}
else if (m.type() == message_t::kConditional) {
print_debug(m, ostr);
ostr << "alt\n";
ostr << "alt";
if (m.condition_text())
ostr << " " << m.condition_text().value();
ostr << '\n';
}
else if (m.type() == message_t::kConditionalElse) {
print_debug(m, ostr);

View File

@@ -57,4 +57,17 @@ void message::set_message_scope(common::model::message_scope_t scope)
common::model::message_scope_t message::message_scope() const { return scope_; }
void message::condition_text(const std::string &condition_text)
{
if (condition_text.empty())
condition_text_ = std::nullopt;
else
condition_text_ = condition_text;
}
std::optional<std::string> message::condition_text() const
{
return condition_text_;
}
} // namespace clanguml::sequence_diagram::model

View File

@@ -128,6 +128,20 @@ public:
*/
common::model::message_scope_t message_scope() const;
/**
* @brief Set condition text for block statements (e.g. if(<THIS TEXT>))
*
* @param condition_text Condition text
*/
void condition_text(const std::string &condition_text);
/**
* @brief Get condition text
*
* @return Block statement condition text
*/
std::optional<std::string> condition_text() const;
private:
common::model::message_t type_{common::model::message_t::kNone};
@@ -143,6 +157,8 @@ private:
std::string message_name_{};
std::string return_type_{};
std::optional<std::string> condition_text_;
};
} // namespace clanguml::sequence_diagram::model

View File

@@ -307,15 +307,25 @@ bool call_expression_context::is_expr_in_current_control_statement_condition(
const clang::Stmt *stmt) const
{
if (current_ifstmt() != nullptr) {
if (common::is_subexpr_of(current_ifstmt()->getCond(), stmt)) {
if (common::is_subexpr_of(current_ifstmt()->getCond(), stmt))
return true;
if (const auto *condition_decl_stmt = current_ifstmt()->getInit();
condition_decl_stmt != nullptr) {
if (common::is_subexpr_of(condition_decl_stmt, stmt))
return true;
}
}
if (current_elseifstmt() != nullptr) {
if (common::is_subexpr_of(current_elseifstmt()->getCond(), stmt)) {
if (common::is_subexpr_of(current_elseifstmt()->getCond(), stmt))
return true;
}
if (current_conditionaloperator() != nullptr) {
if (common::is_subexpr_of(
current_conditionaloperator()->getCond(), stmt))
return true;
}
}
if (const auto *loop_stmt = current_loopstmt(); loop_stmt != nullptr) {

View File

@@ -584,6 +584,10 @@ bool translation_unit_visitor::TraverseIfStmt(clang::IfStmt *stmt)
const auto current_caller_id = context().caller_id();
const auto *current_ifstmt = context().current_ifstmt();
std::string condition_text;
if (config().generate_condition_statements())
condition_text = common::get_condition_text(source_manager(), stmt);
// Check if this is a beginning of a new if statement, or an
// else if condition of the current if statement
if (current_ifstmt != nullptr) {
@@ -601,6 +605,7 @@ bool translation_unit_visitor::TraverseIfStmt(clang::IfStmt *stmt)
message m{message_t::kElseIf, current_caller_id};
set_source_location(*stmt, m);
m.condition_text(condition_text);
diagram().add_block_message(std::move(m));
}
else {
@@ -608,6 +613,7 @@ bool translation_unit_visitor::TraverseIfStmt(clang::IfStmt *stmt)
message m{message_t::kIf, current_caller_id};
set_source_location(*stmt, m);
m.condition_text(condition_text);
diagram().add_block_message(std::move(m));
}
}
@@ -631,10 +637,15 @@ bool translation_unit_visitor::TraverseWhileStmt(clang::WhileStmt *stmt)
const auto current_caller_id = context().caller_id();
std::string condition_text;
if (config().generate_condition_statements())
condition_text = common::get_condition_text(source_manager(), stmt);
if (current_caller_id != 0) {
context().enter_loopstmt(stmt);
message m{message_t::kWhile, current_caller_id};
set_source_location(*stmt, m);
m.condition_text(condition_text);
diagram().add_block_message(std::move(m));
}
RecursiveASTVisitor<translation_unit_visitor>::TraverseWhileStmt(stmt);
@@ -656,10 +667,15 @@ bool translation_unit_visitor::TraverseDoStmt(clang::DoStmt *stmt)
const auto current_caller_id = context().caller_id();
std::string condition_text;
if (config().generate_condition_statements())
condition_text = common::get_condition_text(source_manager(), stmt);
if (current_caller_id != 0) {
context().enter_loopstmt(stmt);
message m{message_t::kDo, current_caller_id};
set_source_location(*stmt, m);
m.condition_text(condition_text);
diagram().add_block_message(std::move(m));
}
@@ -682,10 +698,15 @@ bool translation_unit_visitor::TraverseForStmt(clang::ForStmt *stmt)
const auto current_caller_id = context().caller_id();
std::string condition_text;
if (config().generate_condition_statements())
condition_text = common::get_condition_text(source_manager(), stmt);
if (current_caller_id != 0) {
context().enter_loopstmt(stmt);
message m{message_t::kFor, current_caller_id};
set_source_location(*stmt, m);
m.condition_text(condition_text);
diagram().add_block_message(std::move(m));
}
@@ -761,10 +782,15 @@ bool translation_unit_visitor::TraverseCXXForRangeStmt(
const auto current_caller_id = context().caller_id();
std::string condition_text;
if (config().generate_condition_statements())
condition_text = common::get_condition_text(source_manager(), stmt);
if (current_caller_id != 0) {
context().enter_loopstmt(stmt);
message m{message_t::kFor, current_caller_id};
set_source_location(*stmt, m);
m.condition_text(condition_text);
diagram().add_block_message(std::move(m));
}
@@ -847,10 +873,15 @@ bool translation_unit_visitor::TraverseConditionalOperator(
const auto current_caller_id = context().caller_id();
std::string condition_text;
if (config().generate_condition_statements())
condition_text = common::get_condition_text(source_manager(), stmt);
if (current_caller_id != 0) {
context().enter_conditionaloperator(stmt);
model::message m{message_t::kConditional, current_caller_id};
set_source_location(*stmt, m);
m.condition_text(condition_text);
diagram().add_block_message(std::move(m));
}

View File

@@ -35,7 +35,8 @@ TEST_CASE("t20028", "[test-case][sequence]")
REQUIRE_THAT(puml, EndsWith("@enduml\n"));
// Check if all calls exist
REQUIRE_THAT(puml, HasCall(_A("tmain()"), _A("A"), "a()"));
REQUIRE_THAT(
puml, HasCallInControlCondition(_A("tmain()"), _A("A"), "a()"));
REQUIRE_THAT(puml, HasCall(_A("tmain()"), _A("A"), "b()"));
REQUIRE_THAT(puml, HasCall(_A("tmain()"), _A("A"), "c()"));
REQUIRE_THAT(puml, HasCall(_A("tmain()"), _A("A"), "d()"));

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

@@ -0,0 +1,15 @@
compilation_database_dir: ..
output_directory: puml
diagrams:
t20033_sequence:
type: sequence
glob:
- ../../tests/t20033/t20033.cc
include:
namespaces:
- clanguml::t20033
using_namespace:
- clanguml::t20033
generate_condition_statements: true
start_from:
- function: "clanguml::t20033::tmain()"

65
tests/t20033/t20033.cc Normal file
View File

@@ -0,0 +1,65 @@
#include <cmath>
#include <cstdint>
#include <vector>
namespace clanguml {
namespace t20033 {
struct A {
int a1() { return 0; }
int a2() { return 1; }
int a3() { return 2; }
int a4() { return 3; }
};
int tmain()
{
A a;
int result{};
// clang-format off
if(false) {
result = 0;
}
else if (reinterpret_cast<uint64_t>(&a) % 100 == 0ULL) {
result = a.a1();
}
else if (reinterpret_cast<uint64_t>(&a) % 64 == 0ULL) {
result = a.a2();
}
else if(a.a2() == 2 &&
a.a3() == 3) {
result = a.a3();
}
else {
result = a.a4();
}
// clang-format on
if (int i = a.a2(); i != 2) {
result += a.a3();
}
for (int i = 0; i < a.a2(); i++) {
result += i * a.a3();
}
int retry_count = a.a3();
while (retry_count--) {
result -= a.a2();
}
do {
result += a.a4();
} while (retry_count++ < a.a3());
result = a.a4() % 6 ? result * 2 : result;
std::vector<int> ints;
for (auto i : ints) {
result += a.a4();
}
return result;
}
}
}

58
tests/t20033/test_case.h Normal file
View File

@@ -0,0 +1,58 @@
/**
* tests/t20033/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("t20033", "[test-case][sequence]")
{
auto [config, db] = load_config("t20033");
auto diagram = config.diagrams["t20033_sequence"];
REQUIRE(diagram->name == "t20033_sequence");
auto model = generate_sequence_diagram(*db, diagram);
REQUIRE(model->name() == "t20033_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()"), _A("A"), "a1()"));
REQUIRE_THAT(
puml, HasCallInControlCondition(_A("tmain()"), _A("A"), "a2()"));
REQUIRE_THAT(
puml, HasCallInControlCondition(_A("tmain()"), _A("A"), "a3()"));
REQUIRE_THAT(
puml, HasCallInControlCondition(_A("tmain()"), _A("A"), "a4()"));
save_puml(
config.output_directory() + "/" + diagram->name + ".puml", puml);
}
{
auto j = generate_sequence_json(diagram, *model);
using namespace json;
save_json(config.output_directory() + "/" + diagram->name + ".json", j);
}
}

View File

@@ -346,6 +346,7 @@ using namespace clanguml::test::matchers;
#include "t20030/test_case.h"
#include "t20031/test_case.h"
#include "t20032/test_case.h"
#include "t20033/test_case.h"
///
/// Package diagram tests

View File

@@ -295,6 +295,9 @@ test_cases:
- name: t20032
title: Return type generation option sequence diagram test case
description:
- name: t20033
title: Control statement text in sequence diagram test case
description:
Package diagrams:
- name: t30001
title: Basic package diagram test case