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