0.6.1
C++ to UML diagram generator based on Clang
Loading...
Searching...
No Matches
diagram.cc
Go to the documentation of this file.
1/**
2 * @file src/sequence_diagram/model/diagram.cc
3 *
4 * Copyright (c) 2021-2025 Bartek Kryza <bkryza@gmail.com>
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19#include "diagram.h"
20
22
23#include <functional>
24#include <memory>
25
27
28std::vector<std::vector<eid_t>> find_reverse_message_chains(
30{
31 std::vector<std::vector<eid_t>> all_message_chains;
32 std::vector<eid_t> current_chain;
33
34 // Depth first search lambda function to traverse reverse call graph
35 std::function<void(const reverse_call_graph_activity_node &)> dfs =
36 [&](const reverse_call_graph_activity_node &node) {
37 // Add the current node’s activity ID to the path
38 current_chain.push_back(node.activity_id);
39
40 if (node.callers.empty()) {
41 auto reversed = current_chain;
42 std::reverse(reversed.begin(), reversed.end());
43 all_message_chains.emplace_back(std::move(reversed));
44 }
45 else {
46 for (const auto &child : node.callers) {
47 dfs(child);
48 }
49 }
50
51 // Backtrack
52 current_chain.pop_back();
53 };
54
55 // Start depth first search
56 dfs(root);
57
58 return all_message_chains;
59}
60
62{
64}
65
67 const std::string &full_name) const
68{
69 for (const auto &[id, participant] : participants_) {
70 if (participant->full_name(false) == full_name)
71 return {*participant};
72 }
73
74 return {};
75}
76
78 const eid_t id) const
79{
80 if (participants_.find(id) != participants_.end())
81 return {*participants_.at(id)};
82
83 return {};
84}
85
86std::string diagram::to_alias(const std::string &full_name) const
87{
88 return full_name;
89}
90
91void diagram::add_participant(std::unique_ptr<participant> p)
92{
93 const auto participant_id = p->id();
94
95 p->complete(true);
96
97 assert(participant_id.is_global());
98
99 if (participants_.find(participant_id) == participants_.end()) {
100 LOG_DBG("Adding '{}' participant: {}, {} [{}]", p->type_name(),
101 p->full_name(false), p->id(),
102 p->type_name() == "method"
103 ? dynamic_cast<method *>(p.get())->method_name()
104 : "");
105
106 participants_.emplace(participant_id, std::move(p));
107 }
108}
109
111{
112 active_participants_.emplace(id);
113}
114
116{
117 return activities_.at(id);
118}
119
120bool diagram::has_activity(eid_t id) const { return activities_.count(id) > 0; }
121
123
125{
126 const auto caller_id = message.from();
127 const auto callee_id = message.to();
128
129 if (activities_.find(caller_id) == activities_.end()) {
130 activity a{caller_id};
131 activities_.insert({caller_id, std::move(a)});
132 }
133
134 if (activities_.find(callee_id) == activities_.end()) {
135 activity a{callee_id};
136 activities_.insert({callee_id, std::move(a)});
137 }
138
139 get_activity(callee_id).add_caller(caller_id);
140 get_activity(caller_id).add_message(std::move(message));
141}
142
144{
145 add_message(std::move(message));
146}
147
150{
151 const auto caller_id = message.from();
152
153 if (activities_.find(caller_id) != activities_.end()) {
154 auto &current_messages = get_activity(caller_id).messages();
155
157 std::move(message), start_type, current_messages);
158 }
159}
160
162{
164 const auto caller_id = m.from();
165
166 if (activities_.find(caller_id) != activities_.end()) {
167 auto &current_messages = get_activity(caller_id).messages();
168
169 if (current_messages.back().type() == message_t::kCase) {
170 // Do nothing - fallthroughs not supported yet...
171 }
172 else {
173 current_messages.emplace_back(std::move(m));
174 }
175 }
176}
177
178std::map<eid_t, activity> &diagram::sequences() { return activities_; }
179
180const std::map<eid_t, activity> &diagram::sequences() const
181{
182 return activities_;
183}
184
185std::map<eid_t, std::unique_ptr<participant>> &diagram::participants()
186{
187 return participants_;
188}
189
190const std::map<eid_t, std::unique_ptr<participant>> &
192{
193 return participants_;
194}
195
197
198const std::set<eid_t> &diagram::active_participants() const
199{
201}
202
205{
206 return filter().should_include(p) &&
208 dynamic_cast<const common::model::source_location &>(p));
209}
210
211std::vector<std::string> diagram::list_from_values() const
212{
213 std::vector<std::string> result;
214
215 for (const auto &[from_id, act] : activities_) {
216
217 const auto &from_activity = *(participants_.at(from_id));
218 const auto &full_name = from_activity.full_name(false);
219 if (!full_name.empty())
220 result.push_back(full_name);
221 }
222
223 std::sort(result.begin(), result.end());
224 result.erase(std::unique(result.begin(), result.end()), result.end());
225
226 return result;
227}
228
229std::vector<std::string> diagram::list_to_values() const
230{
231 std::vector<std::string> result;
232
233 for (const auto &[from_id, act] : activities_) {
234 for (const auto &m : act.messages()) {
235 if (participants_.count(m.to()) > 0) {
236 const auto &to_activity = *(participants_.at(m.to()));
237 const auto &full_name = to_activity.full_name(false);
238 if (!full_name.empty())
239 result.push_back(full_name);
240 }
241 }
242 }
243
244 std::sort(result.begin(), result.end());
245 result.erase(std::unique(result.begin(), result.end()), result.end());
246
247 return result;
248}
249
251 const config::source_location &to_location) const
252{
253 std::vector<eid_t> to_activities{};
254
255 for (const auto &[k, v] : sequences()) {
256 for (const auto &m : v.messages()) {
257 if (m.type() != common::model::message_t::kCall)
258 continue;
259
260 const auto &callee = *participants().at(m.to());
261 std::string vto = callee.full_name(false);
262 if (to_location.location == vto) {
263 LOG_DBG(
264 "Found sequence diagram end point '{}': {}", vto, m.to());
265 to_activities.push_back(m.to());
266 }
267 }
268 }
269
270 if (to_activities.empty()) {
271 LOG_WARN("Failed to find 'to' participant {} for to "
272 "condition: ",
273 to_location.location.to_string());
274 }
275
276 util::remove_duplicates(to_activities);
277
278 return to_activities;
279}
280
282 const config::source_location &from_location) const
283{
284 std::vector<eid_t> from_activities{};
285
286 for (const auto &[k, v] : sequences()) {
287 if (participants().count(v.from()) == 0)
288 continue;
289
290 const auto &caller = *participants().at(v.from());
291 std::string vfrom = caller.full_name(false);
292 if (from_location.location == vfrom) {
293 LOG_DBG("Found sequence diagram start point '{}': {}", vfrom, k);
294 from_activities.push_back(k);
295 }
296 }
297
298 if (from_activities.empty()) {
299 LOG_WARN("Failed to find 'from' participant {} for from "
300 "condition: ",
301 from_location.location.to_string());
302 }
303
304 util::remove_duplicates(from_activities);
305
306 return from_activities;
307}
308
310 std::set<eid_t> visited_callers) const
311{
312 LOG_INFO("Building reverse call graph for activity: {}", node.activity_id);
313
314 if (sequences().count(node.activity_id) == 0)
315 return;
316
317 visited_callers.insert(node.activity_id);
318
319 const auto &callers = sequences().at(node.activity_id).callers();
320
321 for (const auto &caller : callers) {
322 if (visited_callers.count(caller) > 0) {
323 // break recursive calls
324 continue;
325 }
326
328 caller_node.activity_id = caller;
329
330 build_reverse_call_graph(caller_node, visited_callers);
331
332 node.callers.emplace_back(std::move(caller_node));
333 }
334}
335
336std::vector<message_chain_t> diagram::get_all_from_to_message_chains(
337 const eid_t from_activity, const eid_t to_activity) const
338{
339 // Message (call) chains matching the specified from_to condition
340 std::vector<message_chain_t> message_chains;
341
342 // First, build reverse call graph starting from target `to_activity`
343 // `target_roots` should contain all activities which call `to_activity`
345 target_roots.activity_id = to_activity;
346
347 for (const auto &[k, v] : sequences()) {
348 for (const auto &m : v.messages()) {
349 if (m.type() != common::model::message_t::kCall)
350 continue;
351
352 if (m.to() == to_activity) {
354 node.activity_id = m.from();
355 target_roots.callers.emplace_back(std::move(node));
356 }
357 }
358 }
359
360 // Now recurse from the initial target activities based on reverse
361 // callers list stored in each activity
362 for (auto &caller : target_roots.callers)
364
365 // Convert the reverse call graph into a list of call chains using
366 // depth first search
367 auto activity_id_chains = find_reverse_message_chains(target_roots);
368
369 // Make sure the activity call chains list is unique
370 sort(begin(activity_id_chains), end(activity_id_chains));
371 activity_id_chains.erase(
372 unique(begin(activity_id_chains), end(activity_id_chains)),
373 end(activity_id_chains));
374
375 // Convert the call chains with activity ids to lists of actual messages
376 for (const auto &chain : activity_id_chains) {
377 message_chain_t message_chain;
378 for (auto it = begin(chain); it != end(chain); it++) {
379 const auto next_it = it + 1;
380 if (next_it == end(chain))
381 break;
382
383 auto from_id = *it;
384 if (activities_.count(from_id) == 0)
385 continue;
386
387 auto to_id = *(next_it);
388
389 const auto &act = activities_.at(from_id);
390
391 for (const auto &m : act.messages()) {
392 if (m.to() == to_id) {
393 message_chain.push_back(m);
394 break;
395 }
396 }
397 }
398 message_chains.emplace_back(std::move(message_chain));
399 }
400
401 // Perform final filtering of the message chains
402 std::vector<message_chain_t> message_chains_filtered{};
403
404 for (auto &mc : message_chains) {
405 // Skip empty chains
406 if (mc.empty())
407 continue;
408
409 // Make sure the chain has valid starting point
410 if (from_activity.value() == 0 ||
411 (mc.front().from() == from_activity)) {
412 message_chains_filtered.push_back(mc);
413 }
414 }
415
416 int message_chain_index{};
417 for (const auto &mc : message_chains_filtered) {
418 LOG_INFO("\t{}: {}", message_chain_index++,
419 fmt::join(util::map<std::string>(mc,
420 [](const model::message &m) -> std::string {
421 return m.message_name();
422 }),
423 "->"));
424 }
425
426 return message_chains_filtered;
427}
428
430{
431 return activities_.empty() || participants_.empty();
432}
433
435{
436 using namespace std::string_literals;
437
438 std::map<eid_t, activity> activities;
439 std::map<eid_t, std::unique_ptr<participant>> participants;
440 std::set<eid_t> active_participants;
441
442 for (auto &[id, act] : sequences()) {
443 model::activity new_activity{id};
444
445 // If activity is a lambda operator() - skip it, we're only processing
446 // normal activities at this level, and descend recursively into
447 // lambda activities when encountering a lambda call expression
448 auto maybe_lambda_activity = get_participant<model::method>(id);
449
450 if (maybe_lambda_activity) {
451 const auto parent_class_id =
452 maybe_lambda_activity.value().class_id();
453 auto maybe_parent_class =
454 get_participant<model::class_>(parent_class_id);
455
456 if (maybe_parent_class && maybe_parent_class.value().is_lambda()) {
457 continue;
458 }
459 }
460
461 // For other activities, check each message - if it calls lambda
462 // operator() - reattach the message to the next activity in the chain
463 // (assuming it's not lambda)
464 for (auto &m : act.messages()) {
465
466 auto message_call_to_lambda{false};
467
468 message_call_to_lambda =
469 inline_lambda_operator_call(id, new_activity, m);
470
471 if (!message_call_to_lambda)
472 new_activity.add_message(m);
473 }
474
475 // Add activity
476 activities.insert({id, std::move(new_activity)});
477 }
478
479 for (auto &&[id, p] : this->participants()) {
480 // Skip participants which are lambda classes
481 if (const auto *maybe_class =
482 dynamic_cast<const model::class_ *>(p.get());
483 maybe_class != nullptr && maybe_class->is_lambda()) {
484 continue;
485 }
486
487 // Skip participants which are lambda operator methods
488 if (const auto *maybe_method =
489 dynamic_cast<const model::method *>(p.get());
490 maybe_method != nullptr) {
491 auto maybe_class =
492 get_participant<model::class_>(maybe_method->class_id());
493 if (maybe_class && maybe_class.value().is_lambda()) {
494 continue;
495 }
496 }
497
498 // Otherwise move the participant to the new diagram model
499 auto participant_id = p->id();
500 participants.emplace(participant_id, std::move(p));
501 }
502
503 // Skip active participants which are not in lambdaless_diagram participants
504 for (auto id : this->active_participants()) {
505 if (participants.count(id) > 0) {
506 active_participants.emplace(id);
507 }
508 }
509
510 activities_ = std::move(activities);
511 participants_ = std::move(participants);
513}
514
516 const eid_t id, model::activity &new_activity, const model::message &m)
517{
518 using namespace std::string_literals;
519
520 bool message_call_to_lambda{false};
521 auto maybe_lambda_operator = get_participant<model::method>(m.to());
522
523 if (maybe_lambda_operator) {
524 const auto parent_class_id = maybe_lambda_operator.value().class_id();
525 auto maybe_parent_class =
526 get_participant<model::class_>(parent_class_id);
527
528 if (maybe_parent_class && maybe_parent_class.value().is_lambda()) {
529 if (has_activity(m.to())) {
530 auto lambda_operator_activity = get_activity(m.to());
531
532 // For each call in that lambda activity - reattach this
533 // call to the current activity
534 for (auto &mm : lambda_operator_activity.messages()) {
535 if (!inline_lambda_operator_call(id, new_activity, mm)) {
536
537 // Do not propagate return calls from nested lambdas
538 if (mm.type() == common::model::message_t::kReturn) {
539 continue;
540 }
541
542 auto new_message{mm};
543
544 new_message.set_from(id);
545
546 new_activity.add_message(new_message);
547 }
548 }
549 }
550
551 message_call_to_lambda = true;
552 }
553 }
554
555 return message_call_to_lambda;
556}
557
558void diagram::print() const
559{
560 LOG_TRACE(" --- Participants ---");
561 for (const auto &[id, participant] : participants_) {
562 LOG_DBG("{} - {}", id, participant->to_string());
563 }
564
565 LOG_TRACE(" --- Activities ---");
566 for (const auto &[from_id, act] : activities_) {
567 if (participants_.count(from_id) == 0)
568 continue;
569
570 LOG_TRACE("Sequence id={}:", from_id);
571
572 const auto &from_activity = *(participants_.at(from_id));
573
574 LOG_TRACE(" Activity id={}, from={}:", act.from(),
575 from_activity.full_name(false));
576
577 for (const auto &message : act.messages()) {
578 if (participants_.find(message.from()) == participants_.end())
579 continue;
580
581 const auto &from_participant = *participants_.at(message.from());
582
584 if (message.to() == 0)
585 LOG_TRACE(
586 " Return from={}, from_id={}, name={}, type={}",
587 from_participant.full_name(false),
588 from_participant.id(), message.message_name(),
589 to_string(message.type()));
590 else {
591 const auto &to_participant =
592 *participants_.at(message.to());
593
594 LOG_TRACE(" Return from={}, from_id={}, "
595 "to={}, to_id={}, name={}, type={}",
596 from_participant.full_name(false),
597 from_participant.id(), to_participant.full_name(false),
599 to_string(message.type()));
600 }
601 }
602 else if (participants_.find(message.to()) == participants_.end()) {
603 LOG_TRACE(" Message from={}, from_id={}, "
604 "to={}, to_id={}, name={}, type={}",
605 from_participant.full_name(false), from_participant.id(),
606 "__UNRESOLVABLE_ID__", message.to(), message.message_name(),
607 to_string(message.type()));
608 }
609 else {
610 const auto &to_participant = *participants_.at(message.to());
611
612 std::string message_comment{"None"};
613 if (const auto &cmt = message.comment(); cmt.has_value()) {
614 message_comment = cmt.value().at("comment");
615 }
616
617 LOG_TRACE(" Message from={}, from_id={}, "
618 "to={}, to_id={}, name={}, type={}, comment={}",
619 from_participant.full_name(false), from_participant.id(),
620 to_participant.full_name(false), to_participant.id(),
621 message.message_name(), to_string(message.type()),
622 message_comment);
623 }
624 }
625 }
626}
627
629 const common::model::message_t statement_begin,
630 std::vector<message> &current_messages) const
631{
632 bool is_empty_statement{true};
633
634 auto rit = current_messages.rbegin();
635 for (; rit != current_messages.rend(); rit++) {
636 if (rit->type() == statement_begin) {
637 break;
638 }
639 if (rit->type() == common::model::message_t::kReturn) {
640 is_empty_statement = false;
641 break;
642 }
643 if (rit->type() == common::model::message_t::kCoReturn) {
644 is_empty_statement = false;
645 break;
646 }
647 if (rit->type() == common::model::message_t::kCall) {
648 is_empty_statement = false;
649 break;
650 }
651 }
652
653 if (is_empty_statement) {
654 current_messages.erase((rit + 1).base(), current_messages.end());
655 }
656 else {
657 current_messages.emplace_back(std::move(m));
658 }
659}
660
662{
663 // Apply diagram filters and remove any empty block statements
665
666 // First in each sequence (activity) filter out any remaining
667 // uninteresting calls
668 for (auto &[id, act] : activities_) {
669 util::erase_if(act.messages(), [this](auto &m) {
670 if (m.type() != message_t::kCall)
671 return false;
672
673 const auto &to = get_participant<model::participant>(m.to());
674 if (!to || to.value().skip())
675 return true;
676
677 if (!should_include(to.value())) {
678 LOG_DBG("Excluding call from [{}] to {} [{}]", m.from(),
679 to.value().full_name(false), m.to());
680 return true;
681 }
682
683 return false;
684 });
685 }
686
687 // Now remove any empty block statements, e.g. if/endif
688 for (auto &[id, act] : activities_) {
689 int64_t block_nest_level{0};
690 std::vector<std::vector<message>> block_message_stack;
691 // Add first stack level - this level will contain the filtered
692 // message sequence
693 block_message_stack.emplace_back();
694
695 // First create a recursive stack from the messages and
696 // message blocks (e.g. if statements)
697 for (auto &m : act.messages()) {
698 if (is_begin_block_message(m.type())) {
699 block_nest_level++;
700 block_message_stack.push_back({m});
701 }
702 else if (is_end_block_message(m.type())) {
703 block_nest_level--;
704
705 block_message_stack.back().push_back(m);
706
707 // Check the last stack for any calls, if yes, collapse it
708 // on the previous stack
709 if (std::count_if(block_message_stack.back().begin(),
710 block_message_stack.back().end(), [](auto &m) {
711 return (m.type() == message_t::kCall) ||
712 (m.type() == message_t::kReturn) ||
713 (m.type() == message_t::kCoReturn);
714 }) > 0) {
715 std::copy(block_message_stack.back().begin(),
716 block_message_stack.back().end(),
717 std::back_inserter(
718 block_message_stack.at(block_nest_level)));
719 }
720
721 block_message_stack.pop_back();
722
723 assert(block_nest_level >= 0);
724 }
725 else {
726 if (m.type() == message_t::kCall) {
727 // Set the message return type based on the callee return
728 // type
729 auto to_participant =
730 get_participant<sequence_diagram::model::function>(
731 m.to());
732 if (to_participant.has_value()) {
733 m.set_return_type(to_participant.value().return_type());
734 }
735 }
736 block_message_stack.back().push_back(m);
737 }
738 }
739
740 act.messages().clear();
741
742 for (auto &m : block_message_stack[0]) {
743 act.add_message(m);
744 }
745 }
746}
747} // namespace clanguml::sequence_diagram::model
748
749namespace clanguml::common::model {
750template <>
751bool check_diagram_type<clanguml::sequence_diagram::model::diagram>(diagram_t t)
752{
753 return t == diagram_t::kSequence;
754}
755}