Added option inline_lambda_messages to omit lambda expressions from sequence diagrams (#261)

This commit is contained in:
Bartek Kryza
2024-05-03 14:42:15 +02:00
parent 052d9b7ff3
commit 14a13b45aa
18 changed files with 498 additions and 42 deletions

View File

@@ -561,6 +561,9 @@ cli_flow_t cli_handler::add_config_diagram(
doc["diagrams"][name]["glob"] = std::vector<std::string>{{"src/*.cpp"}};
doc["diagrams"][name]["combine_free_functions_into_file_participants"] =
true;
doc["diagrams"][name]["inline_lambda_messages"] = false;
doc["diagrams"][name]["generate_message_comments"] = false;
doc["diagrams"][name]["generate_condition_statements"] = false;
doc["diagrams"][name]["using_namespace"] =
std::vector<std::string>{{"myproject"}};
doc["diagrams"][name]["include"]["paths"] =

View File

@@ -40,6 +40,11 @@ class diagram {
public:
diagram();
diagram(const diagram &) = delete;
diagram(diagram && /*unused*/) noexcept;
diagram &operator=(const diagram &) = delete;
diagram &operator=(diagram && /*unused*/) noexcept;
virtual ~diagram();
/**
@@ -77,11 +82,6 @@ public:
virtual common::optional_ref<clanguml::common::model::diagram_element>
get_with_namespace(const std::string &name, const namespace_ &ns) const;
diagram(const diagram &) = delete;
diagram(diagram && /*unused*/) noexcept;
diagram &operator=(const diagram &) = delete;
diagram &operator=(diagram && /*unused*/) noexcept;
/**
* Set diagram name.
*

View File

@@ -236,6 +236,7 @@ void inheritable_diagram_options::inherit(
comment_parser.override(parent.comment_parser);
combine_free_functions_into_file_participants.override(
parent.combine_free_functions_into_file_participants);
inline_lambda_messages.override(parent.inline_lambda_messages);
generate_return_types.override(parent.generate_return_types);
generate_condition_statements.override(
parent.generate_condition_statements);

View File

@@ -570,6 +570,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> inline_lambda_messages{"inline_lambda_messages", false};
option<bool> generate_return_types{"generate_return_types", false};
option<bool> generate_condition_statements{
"generate_condition_statements", false};

View File

@@ -218,6 +218,7 @@ types:
#
generate_method_arguments: !optional generate_method_arguments_t
combine_free_functions_into_file_participants: !optional bool
inline_lambda_messages: !optional bool
generate_return_types: !optional bool
generate_condition_statements: !optional bool
generate_message_comments: !optional bool
@@ -340,6 +341,7 @@ root:
include_relations_also_as_members: !optional bool
generate_method_arguments: !optional generate_method_arguments_t
combine_free_functions_into_file_participants: !optional bool
inline_lambda_messages: !optional bool
generate_concept_requirements: !optional bool
generate_return_types: !optional bool
generate_condition_statements: !optional bool

View File

@@ -665,6 +665,7 @@ template <> struct convert<sequence_diagram> {
get_option(node, rhs.from_to);
get_option(node, rhs.to);
get_option(node, rhs.combine_free_functions_into_file_participants);
get_option(node, rhs.inline_lambda_messages);
get_option(node, rhs.generate_return_types);
get_option(node, rhs.generate_condition_statements);
get_option(node, rhs.participants_order);
@@ -844,6 +845,7 @@ template <> struct convert<config> {
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.inline_lambda_messages);
get_option(node, rhs.generate_return_types);
get_option(node, rhs.generate_condition_statements);
get_option(node, rhs.generate_message_comments);

View File

@@ -347,6 +347,7 @@ YAML::Emitter &operator<<(
sd != nullptr) {
out << sd->title;
out << c.combine_free_functions_into_file_participants;
out << c.inline_lambda_messages;
out << c.generate_condition_statements;
out << c.generate_method_arguments;
out << c.generate_return_types;

View File

@@ -125,8 +125,8 @@ 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
// 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;
@@ -338,7 +338,8 @@ void generator::generate_participant(
auto p = model().get(name);
if (!p.has_value()) {
LOG_WARN("Cannot find participant {} from `participants_order` option",
LOG_WARN("Cannot find participant {} from `participants_order` "
"option",
name);
return;
}
@@ -629,10 +630,10 @@ void generator::generate_diagram(std::ostream &ostr) const
model::function::message_render_mode render_mode =
select_method_arguments_render_mode();
// For methods or functions in diagrams where they are combined into
// file participants, we need to add an 'entry' point call to know
// which method relates to the first activity for this 'start_from'
// condition
// For methods or functions in diagrams where they are
// combined into file participants, we need to add an
// 'entry' point call to know which method relates to the
// first activity for this 'start_from' condition
if (from.value().type_name() == "method" ||
config().combine_free_functions_into_file_participants()) {
ostr << "[->"

View File

@@ -95,22 +95,22 @@ void diagram::add_active_participant(common::id_t id)
const activity &diagram::get_activity(common::id_t id) const
{
return sequences_.at(id);
return activities_.at(id);
}
bool diagram::has_activity(common::id_t id) const
{
return sequences_.count(id) > 0;
return activities_.count(id) > 0;
}
activity &diagram::get_activity(common::id_t id) { return sequences_.at(id); }
activity &diagram::get_activity(common::id_t id) { return activities_.at(id); }
void diagram::add_message(model::message &&message)
{
const auto caller_id = message.from();
if (sequences_.find(caller_id) == sequences_.end()) {
if (activities_.find(caller_id) == activities_.end()) {
activity a{caller_id};
sequences_.insert({caller_id, std::move(a)});
activities_.insert({caller_id, std::move(a)});
}
get_activity(caller_id).add_message(std::move(message));
@@ -126,7 +126,7 @@ void diagram::end_block_message(
{
const auto caller_id = message.from();
if (sequences_.find(caller_id) != sequences_.end()) {
if (activities_.find(caller_id) != activities_.end()) {
auto &current_messages = get_activity(caller_id).messages();
fold_or_end_block_statement(
@@ -139,7 +139,7 @@ void diagram::add_case_stmt_message(model::message &&m)
using clanguml::common::model::message_t;
const auto caller_id = m.from();
if (sequences_.find(caller_id) != sequences_.end()) {
if (activities_.find(caller_id) != activities_.end()) {
auto &current_messages = get_activity(caller_id).messages();
if (current_messages.back().type() == message_t::kCase) {
@@ -151,11 +151,11 @@ void diagram::add_case_stmt_message(model::message &&m)
}
}
std::map<common::id_t, activity> &diagram::sequences() { return sequences_; }
std::map<common::id_t, activity> &diagram::sequences() { return activities_; }
const std::map<common::id_t, activity> &diagram::sequences() const
{
return sequences_;
return activities_;
}
std::map<common::id_t, std::unique_ptr<participant>> &diagram::participants()
@@ -191,7 +191,7 @@ std::vector<std::string> diagram::list_from_values() const
{
std::vector<std::string> result;
for (const auto &[from_id, act] : sequences_) {
for (const auto &[from_id, act] : activities_) {
const auto &from_activity = *(participants_.at(from_id));
const auto &full_name = from_activity.full_name(false);
@@ -209,7 +209,7 @@ std::vector<std::string> diagram::list_to_values() const
{
std::vector<std::string> result;
for (const auto &[from_id, act] : sequences_) {
for (const auto &[from_id, act] : activities_) {
for (const auto &m : act.messages()) {
if (participants_.count(m.to()) > 0) {
const auto &to_activity = *(participants_.at(m.to()));
@@ -406,7 +406,7 @@ std::vector<message_chain_t> diagram::get_all_from_to_message_chains(
bool diagram::is_empty() const
{
return sequences_.empty() || participants_.empty();
return activities_.empty() || participants_.empty();
}
void diagram::print() const
@@ -417,7 +417,7 @@ void diagram::print() const
}
LOG_TRACE(" --- Activities ---");
for (const auto &[from_id, act] : sequences_) {
for (const auto &[from_id, act] : activities_) {
if (participants_.count(from_id) == 0)
continue;
@@ -487,7 +487,7 @@ void diagram::finalize()
// First in each sequence (activity) filter out any remaining
// uninteresting calls
for (auto &[id, act] : sequences_) {
for (auto &[id, act] : activities_) {
util::erase_if(act.messages(), [this](auto &m) {
if (m.type() != message_t::kCall)
return false;
@@ -507,7 +507,7 @@ void diagram::finalize()
}
// Now remove any empty block statements, e.g. if/endif
for (auto &[id, act] : sequences_) {
for (auto &[id, act] : activities_) {
int64_t block_nest_level{0};
std::vector<std::vector<message>> block_message_stack;
// Add first stack level - this level will contain the filtered

View File

@@ -295,6 +295,13 @@ public:
*/
bool is_empty() const override;
void update_sequences_from_diagram(diagram &other)
{
activities_ = std::move(other.activities_);
participants_ = std::move(other.participants_);
active_participants_ = std::move(other.active_participants_);
}
private:
/**
* This method checks the last messages in sequence (current_messages),
@@ -337,7 +344,7 @@ private:
return block_end_types.count(mt) > 0;
};
std::map<common::id_t, activity> sequences_;
std::map<common::id_t, activity> activities_;
std::map<common::id_t, std::unique_ptr<participant>> participants_;

View File

@@ -630,7 +630,7 @@ bool translation_unit_visitor::TraverseCXXConstructExpr(
translation_unit_visitor::VisitCXXConstructExpr(expr);
LOG_TRACE("Leaving member call expression at {}",
LOG_TRACE("Leaving cxx construct call expression at {}",
expr->getBeginLoc().printToString(source_manager()));
context().leave_callexpr();
@@ -2018,6 +2018,38 @@ void translation_unit_visitor::pop_message_to_diagram(
}
void translation_unit_visitor::finalize()
{
resolve_ids_to_global();
// 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...
ensure_lambda_messages_have_operator_as_target();
if (config().inline_lambda_messages())
inline_lambda_operator_calls();
}
void translation_unit_visitor::ensure_lambda_messages_have_operator_as_target()
{
for (auto &[id, activity] : diagram().sequences()) {
for (auto &m : activity.messages()) {
auto participant = diagram().get_participant<model::class_>(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()");
}
}
}
}
void translation_unit_visitor::resolve_ids_to_global()
{
std::set<common::id_t> active_participants_unique;
@@ -2041,25 +2073,116 @@ 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<model::class_>(m.to());
void translation_unit_visitor::inline_lambda_operator_calls()
{ // If option to inline lambda calls is enabled, we need to modify the
// sequences to skip the lambda calls. In case lambda call does not lead
// to a non-lambda call, omit it entirely
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());
model::diagram lambdaless_diagram;
m.set_to(participant.value().lambda_operator_id());
m.set_message_name("operator()");
for (auto &[id, act] : diagram().sequences()) {
model::activity new_activity{id};
// If activity is a lambda operator() - skip it
auto maybe_lambda_activity =
diagram().get_participant<model::method>(id);
if (maybe_lambda_activity) {
const auto parent_class_id =
maybe_lambda_activity.value().class_id();
auto maybe_parent_class =
diagram().get_participant<model::class_>(parent_class_id);
if (maybe_parent_class && maybe_parent_class.value().is_lambda()) {
continue;
}
}
// For other activities, check each message - if it calls lambda
// operator() - reattach the message to the next activity in the chain
// (assuming it's not lambda)
for (auto &m : act.messages()) {
auto message_call_to_lambda{false};
message_call_to_lambda =
inline_lambda_operator_call(id, new_activity, m);
if (!message_call_to_lambda)
new_activity.add_message(m);
}
// Add activity
lambdaless_diagram.sequences().insert({id, std::move(new_activity)});
}
for (auto &&[id, p] : diagram().participants()) {
// Skip participants which are lambda classes
if (const auto *maybe_class =
dynamic_cast<const model::class_ *>(p.get());
maybe_class && maybe_class->is_lambda()) {
continue;
}
// Skip participants which are lambda operator methods
if (const auto *maybe_method =
dynamic_cast<const model::method *>(p.get());
maybe_method) {
auto maybe_class = diagram().get_participant<model::class_>(
maybe_method->class_id());
if (maybe_class && maybe_class.value().is_lambda())
continue;
}
// Otherwise move the participant to the new diagram model
lambdaless_diagram.add_participant(std::move(p));
}
// Skip active participants which are not in lambdaless_diagram participants
for (auto id : diagram().active_participants()) {
if (lambdaless_diagram.participants().count(id)) {
lambdaless_diagram.add_active_participant(id);
}
}
diagram().update_sequences_from_diagram(lambdaless_diagram);
}
bool translation_unit_visitor::inline_lambda_operator_call(
const long id, model::activity &new_activity, const model::message &m)
{
bool message_call_to_lambda{false};
auto maybe_lambda_operator =
diagram().get_participant<model::method>(m.to());
if (maybe_lambda_operator) {
const auto parent_class_id = maybe_lambda_operator.value().class_id();
auto maybe_parent_class =
diagram().get_participant<model::class_>(parent_class_id);
if (maybe_parent_class && maybe_parent_class.value().is_lambda()) {
// auto new_message{m};
// new_message.set_
auto lambda_operator_activity = diagram().get_activity(m.to());
// For each call in that lambda activity - reattach this
// call to the current activity
for (auto &mm : lambda_operator_activity.messages()) {
if (!inline_lambda_operator_call(id, new_activity, mm)) {
auto new_message{mm};
new_message.set_from(id);
new_activity.add_message(new_message);
}
}
message_call_to_lambda = true;
}
}
return message_call_to_lambda;
}
std::unique_ptr<clanguml::sequence_diagram::model::method>

View File

@@ -487,6 +487,11 @@ private:
*/
template_builder_t &tbuilder() { return template_builder_; }
void inline_lambda_operator_calls();
bool inline_lambda_operator_call(
const long id, model::activity &new_activity, const model::message &m);
call_expression_context call_expression_context_;
/**
@@ -522,5 +527,7 @@ private:
processed_comments_by_caller_id_;
template_builder_t template_builder_;
void resolve_ids_to_global();
void ensure_lambda_messages_have_operator_as_target();
};
} // namespace clanguml::sequence_diagram::visitor