0.6.2
C++ to UML diagram generator based on Clang
Loading...
Searching...
No Matches
sequence_diagram_generator.cc
Go to the documentation of this file.
1/**
2 * @file src/sequence_diagram/generators/plantuml/sequence_diagram_generator.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
20
21#include "util/error.h"
22#include "util/levenshtein.h"
23
25
31
32using namespace clanguml::util;
33
34//
35// generator
36//
37
41{
42}
43
44void generator::generate_call(const message &m, std::ostream &ostr) const
45{
46 const auto &from = model().get_participant<model::participant>(m.from());
47 const auto &to = model().get_participant<model::participant>(m.to());
48
49 if (!from || !to) {
50 LOG_DBG("Skipping empty call from '{}' to '{}'", m.from(), m.to());
51 return;
52 }
53
54 generate_participant(ostr, m.from());
55 generate_participant(ostr, m.to());
56
57 std::string message;
58
61
62 if (to.value().type_name() == "method") {
63 const auto &f = dynamic_cast<const model::method &>(to.value());
64 const std::string_view style = f.is_static() ? "__" : "";
65
66 if (m.type() == message_t::kCoAwait)
67 message = fmt::format("{}<< co_await >>\\n{}{}", style,
68 f.message_name(render_mode), style);
69 else
70 message = fmt::format(
71 "{}{}{}", style, f.message_name(render_mode), style);
72 }
73 else if (to.value().type_name() == "objc_method") {
74 const auto &f = dynamic_cast<const model::objc_method &>(to.value());
75 const std::string_view style = f.is_static() ? "__" : "";
76 message =
77 fmt::format("{}{}{}", style, f.message_name(render_mode), style);
78 }
79 else if (config().combine_free_functions_into_file_participants()) {
80 if (to.value().type_name() == "function") {
81 const auto &f = dynamic_cast<const model::function &>(to.value());
82 message = f.message_name(render_mode);
83
84 if (f.is_cuda_kernel())
85 message = fmt::format("<< CUDA Kernel >>\\n{}", message);
86 else if (f.is_cuda_device())
87 message = fmt::format("<< CUDA Device >>\\n{}", message);
88 else if (f.is_coroutine())
89 message = fmt::format("<< Coroutine >>\\n{}", message);
90 }
91 else if (to.value().type_name() == "function_template") {
92 const auto &f = dynamic_cast<const model::function &>(to.value());
93 message = f.message_name(render_mode);
94
95 if (f.is_cuda_kernel())
96 message = fmt::format("<< CUDA Kernel >>\\n{}", message);
97 else if (f.is_cuda_device())
98 message = fmt::format("<< CUDA Device >>\\n{}", message);
99 else if (f.is_coroutine())
100 message = fmt::format("<< Coroutine >>\\n{}", message);
101 }
102 }
103
104 message = config().simplify_template_type(message);
105
106 const std::string from_alias = generate_alias(from.value());
107 const std::string to_alias = generate_alias(to.value());
108
109 print_debug(m, ostr);
110
112
113 ostr << from_alias << " "
114 << common::generators::plantuml::to_plantuml(message_t::kCall) << " ";
115
116 ostr << to_alias;
117
118 if (config().generate_links) {
120 }
121
122 ostr << " : ";
123
125 ostr << "**[**";
126
128
130 ostr << "**]**";
131
132 ostr << '\n';
133
134 LOG_DBG("Generated call '{}' from {} [{}] to {} [{}]", message,
135 from.value().full_name(false), m.from(), to.value().full_name(false),
136 m.to());
137}
138
139void generator::generate_return(const message &m, std::ostream &ostr) const
140{
141 // Add return activity only for messages between different actors
142 // and only if the return type is different than void
143 if (m.from() == m.to())
144 return;
145
146 std::string message_stereotype;
147 if (m.type() == message_t::kCoReturn) {
148 message_stereotype = "<< co_return >>";
149 }
150 else if (m.type() == message_t::kCoYield) {
151 message_stereotype = "<< co_yield >>";
152 }
153
154 std::string message_label;
155
156 const auto &from = model().get_participant<model::function>(m.from());
157 const auto &to = model().get_participant<model::participant>(m.to());
158 if (to.has_value() && from.has_value() && !from.value().is_void()) {
159 const std::string from_alias = generate_alias(from.value());
160
161 const std::string to_alias = generate_alias(to.value());
162
163 ostr << from_alias << " "
164 << common::generators::plantuml::to_plantuml(message_t::kReturn)
165 << " " << to_alias;
166
167 if (config().generate_return_types())
168 message_label = render_message_name(m.return_type());
169 else if (config().generate_return_values())
170 message_label = render_message_name(m.message_name());
171 }
172 else if (from.has_value() && !from.value().is_void() &&
173 (from.value().type_name() == "method" ||
174 from.value().type_name() == "objc_method" ||
175 config().combine_free_functions_into_file_participants())) {
176 const std::string from_alias = generate_alias(from.value());
177
178 ostr << "[<--" << " " << from_alias;
179 if (config().generate_return_types())
180 message_label = render_message_name(from.value().return_type());
181 else if (config().generate_return_values())
182 message_label = render_message_name(m.message_name());
183 }
184
185 if (!message_stereotype.empty()) {
186 if (message_label.empty())
187 message_label = fmt::format("//{}//", message_stereotype);
188 else
189 message_label = fmt::format(
190 "//{}//\\n//{}//", message_stereotype, message_label);
191 }
192 else {
193 if (!message_label.empty())
194 message_label = fmt::format("//{}//", message_label);
195 }
196
197 if (!message_label.empty())
198 ostr << " : " << message_label;
199
200 ostr << '\n';
201}
202
204 eid_t activity_id, std::ostream &ostr, std::vector<eid_t> &visited) const
205{
206 const auto &a = model().get_activity(activity_id);
207
208 const auto [it, inserted] = generated_activities_.emplace(activity_id);
209
210 if (config().fold_repeated_activities() && !inserted &&
211 !a.messages().empty()) {
212 const auto &p =
213 model().get_participant<model::participant>(activity_id);
214
215 if (p.has_value()) {
216 ostr << "hnote over " << generate_alias(p.value()) << " : *\n";
217 // This is necessary to keep the hnote over the activity life line
218 ostr << generate_alias(p.value()) << "-[hidden]->"
219 << generate_alias(p.value()) << '\n';
220 }
221
222 return;
223 }
224
225 for (const auto &m : a.messages()) {
226 if (m.in_static_declaration_context()) {
228 continue;
229
231 }
232
233 if (m.type() == message_t::kCall || m.type() == message_t::kCoAwait) {
234 const auto &to =
235 model().get_participant<model::participant>(m.to());
236
237 if (!to.has_value()) {
238 LOG_DBG("Skipping activity {} due to missing target paricipant "
239 "in the diagram",
240 m.from());
241 continue;
242 }
243
244 visited.push_back(m.from());
245
246 LOG_DBG("Generating message [{}] --> [{}]", m.from(), m.to());
247
248 generate_call(m, ostr);
249
250 std::string to_alias = generate_alias(to.value());
251
252 ostr << "activate " << to_alias << '\n';
253
254 if (model().sequences().find(m.to()) != model().sequences().end()) {
255 if (std::find(visited.begin(), visited.end(), m.to()) ==
256 visited
257 .end()) { // break infinite recursion on recursive calls
258
259 LOG_DBG("Generating activity {} (called from {})", m.to(),
260 m.from());
261
262 generate_activity(m.to(), ostr, visited);
263 }
264 }
265 else
266 LOG_DBG("Skipping activity {} --> {} - missing sequence {}",
267 m.from(), m.to(), m.to());
268
269 ostr << "deactivate " << to_alias << '\n';
270
271 visited.pop_back();
272 }
273 else if (m.type() == message_t::kReturn) {
274 print_debug(m, ostr);
276 auto return_message = m;
277 if (!visited.empty()) {
278 return_message.set_to(visited.back());
279 }
280 generate_return(return_message, ostr);
281 }
282 else if (m.type() == message_t::kCoReturn) {
283 print_debug(m, ostr);
285 auto return_message = m;
286 if (!visited.empty()) {
287 return_message.set_to(visited.back());
288 }
289 generate_return(return_message, ostr);
290 }
291 else if (m.type() == message_t::kCoYield) {
292 print_debug(m, ostr);
294 auto return_message = m;
295 if (!visited.empty()) {
296 return_message.set_to(visited.back());
297 }
298 generate_return(return_message, ostr);
299 }
300 else if (m.type() == message_t::kIf) {
301 print_debug(m, ostr);
303 ostr << "alt";
304 if (const auto &text = m.condition_text(); text.has_value())
305 ostr << " " << text.value();
306 ostr << '\n';
307 }
308 else if (m.type() == message_t::kElseIf) {
309 print_debug(m, ostr);
310 ostr << "else";
311 if (const auto &text = m.condition_text(); text.has_value())
312 ostr << " " << text.value();
313 ostr << '\n';
314 }
315 else if (m.type() == message_t::kElse) {
316 print_debug(m, ostr);
317 ostr << "else\n";
318 }
319 else if (m.type() == message_t::kIfEnd) {
320 ostr << "end\n";
321 }
322 else if (m.type() == message_t::kWhile) {
323 print_debug(m, ostr);
325 ostr << "loop";
326 if (const auto &text = m.condition_text(); text.has_value())
327 ostr << " " << text.value();
328 ostr << '\n';
329 }
330 else if (m.type() == message_t::kWhileEnd) {
331 ostr << "end\n";
332 }
333 else if (m.type() == message_t::kFor) {
334 print_debug(m, ostr);
336 ostr << "loop";
337 if (const auto &text = m.condition_text(); text.has_value())
338 ostr << " " << text.value();
339 ostr << '\n';
340 }
341 else if (m.type() == message_t::kForEnd) {
342 ostr << "end\n";
343 }
344 else if (m.type() == message_t::kDo) {
345 print_debug(m, ostr);
347 ostr << "loop";
348 if (const auto &text = m.condition_text(); text.has_value())
349 ostr << " " << text.value();
350 ostr << '\n';
351 }
352 else if (m.type() == message_t::kDoEnd) {
353 ostr << "end\n";
354 }
355 else if (m.type() == message_t::kTry) {
356 print_debug(m, ostr);
358 ostr << "group try\n";
359 }
360 else if (m.type() == message_t::kCatch) {
361 print_debug(m, ostr);
362 ostr << "else " << render_message_name(m.message_name()) << '\n';
363 }
364 else if (m.type() == message_t::kTryEnd) {
365 print_debug(m, ostr);
366 ostr << "end\n";
367 }
368 else if (m.type() == message_t::kSwitch) {
369 print_debug(m, ostr);
371 ostr << "group switch\n";
372 }
373 else if (m.type() == message_t::kCase) {
374 print_debug(m, ostr);
375 ostr << "else " << render_message_name(m.message_name()) << '\n';
376 }
377 else if (m.type() == message_t::kSwitchEnd) {
378 ostr << "end\n";
379 }
380 else if (m.type() == message_t::kConditional) {
381 print_debug(m, ostr);
383 ostr << "alt";
384 if (const auto &text = m.condition_text(); text.has_value())
385 ostr << " " << text.value();
386 ostr << '\n';
387 }
388 else if (m.type() == message_t::kConditionalElse) {
389 print_debug(m, ostr);
390 ostr << "else\n";
391 }
392 else if (m.type() == message_t::kConditionalEnd) {
393 ostr << "end\n";
394 }
395 }
396}
397
399 std::ostream &ostr, const model::message &m) const
400{
401 const auto &from = model().get_participant<model::participant>(m.from());
402 if (!from)
403 return;
404
405 // First generate message comments from \note directives in comments
406 bool comment_generated_from_note_decorators{false};
407 for (const auto &decorator : m.decorators()) {
408 auto note = std::dynamic_pointer_cast<decorators::note>(decorator);
409 if (note && note->applies_to_diagram(config().name)) {
410 comment_generated_from_note_decorators = true;
411
412 ostr << "note over " << generate_alias(from.value()) << '\n';
413
415 note->text, config().message_comment_width())
416 << '\n';
417
418 ostr << "end note" << '\n';
419 }
420 }
421
422 if (comment_generated_from_note_decorators)
423 return;
424
425 if (!config().generate_message_comments())
426 return;
427
428 // Now generate message notes from raw comments if enabled
429 if (const auto &comment = m.comment(); comment &&
430 generated_comment_ids_.emplace(comment.value().at("id")).second) {
431
432 ostr << "note over " << generate_alias(from.value()) << '\n';
433
434 ostr << util::format_message_comment(comment.value().at("comment"),
435 config().message_comment_width())
436 << '\n';
437
438 ostr << "end note" << '\n';
439 }
440}
441
443 std::ostream &ostr, const std::string &name) const
444{
445 auto p = model().get(name);
446
447 if (!p.has_value()) {
448 LOG_WARN("Cannot find participant {} from `participants_order` "
449 "option",
450 name);
451 return;
452 }
453
454 generate_participant(ostr, p.value().id(), true);
455}
456
458 std::ostream &ostr, eid_t id, bool force) const
459{
460 eid_t participant_id{};
461
462 if (!force) {
463 for (const auto pid : model().active_participants()) {
464 if (pid == id) {
465 participant_id = pid;
466 break;
467 }
468 }
469 }
470 else
471 participant_id = id;
472
473 if (participant_id == 0)
474 return;
475
476 if (is_participant_generated(participant_id))
477 return;
478
479 const auto &participant =
480 model().get_participant<model::participant>(participant_id).value();
481
482 if (participant.type_name() == "method") {
483 const auto class_id =
484 model()
485 .get_participant<model::method>(participant_id)
486 .value()
487 .class_id();
488
489 if (is_participant_generated(class_id))
490 return;
491
492 const auto &class_participant =
493 model().get_participant<model::participant>(class_id).value();
494
495 print_debug(class_participant, ostr);
496
497 auto participant_name = config().simplify_template_type(
498 display_name_adapter(class_participant).full_name(false));
499 participant_name =
500 config().using_namespace().relative(participant_name);
501
503
504 ostr << "participant \"" << participant_name << "\" as "
505 << class_participant.alias();
506
507 if (config().generate_links) {
509 ostr, class_participant);
510 }
511
512 ostr << '\n';
513
514 generated_participants_.emplace(class_id);
515 }
516 else if (participant.type_name() == "objc_method") {
517 const auto class_id =
518 model()
519 .get_participant<model::objc_method>(participant_id)
520 .value()
521 .class_id();
522
523 if (is_participant_generated(class_id))
524 return;
525
526 const auto &class_participant =
527 model().get_participant<model::participant>(class_id).value();
528
529 print_debug(class_participant, ostr);
530
531 auto participant_name = config().simplify_template_type(
532 display_name_adapter(class_participant).full_name(false));
533 participant_name =
534 config().using_namespace().relative(participant_name);
535
537
538 ostr << "participant \"" << participant_name << "\" as "
539 << class_participant.alias() << " <<ObjC Interface>>";
540
541 if (config().generate_links) {
543 ostr, class_participant);
544 }
545
546 ostr << '\n';
547
548 generated_participants_.emplace(class_id);
549 }
550 else if ((participant.type_name() == "function" ||
551 participant.type_name() == "function_template") &&
552 config().combine_free_functions_into_file_participants()) {
553 // Create a single participant for all functions declared in a
554 // single file
555 const auto &file_path =
556 model()
557 .get_participant<model::function>(participant_id)
558 .value()
559 .file();
560
561 assert(!file_path.empty());
562
563 const auto file_id = common::to_id(file_path);
564
565 if (is_participant_generated(file_id))
566 return;
567
568 auto participant_name = util::path_to_url(relative(
569 std::filesystem::path{file_path}, config().root_directory())
570 .string());
571
572 ostr << "participant \"" << participant_name << "\" as "
573 << fmt::format("C_{:022}", file_id.value());
574
575 ostr << '\n';
576
577 generated_participants_.emplace(file_id);
578 }
579 else {
580 print_debug(participant, ostr);
581
582 auto participant_name =
583 config().using_namespace().relative(config().simplify_template_type(
584 display_name_adapter(participant).full_name(false)));
586
587 ostr << "participant \"" << participant_name << "\" as "
588 << participant.alias();
589
590 if (const auto *function_ptr =
591 dynamic_cast<const model::function *>(&participant);
592 function_ptr) {
593 if (function_ptr->is_cuda_kernel())
594 ostr << " << CUDA Kernel >>";
595 else if (function_ptr->is_cuda_device())
596 ostr << " << CUDA Device >>";
597 else if (function_ptr->is_coroutine())
598 ostr << fmt::format("<< Coroutine >>");
599 }
600
601 if (config().generate_links) {
603 ostr, participant);
604 }
605
606 ostr << '\n';
607
608 generated_participants_.emplace(participant_id);
609 }
610}
611
613{
614 return std::find(generated_participants_.begin(),
616 id) != generated_participants_.end();
617}
618
620 const model::participant &participant) const
621{
622 if ((participant.type_name() == "function" ||
623 participant.type_name() == "function_template") &&
624 config().combine_free_functions_into_file_participants()) {
625 const auto file_id = common::to_id(participant.file());
626
627 return fmt::format("C_{:022}", file_id.value());
628 }
629
630 return participant.alias();
631}
632
633void generator::generate_diagram(std::ostream &ostr) const
634{
635 model().print();
636
637 if (config().participants_order.has_value) {
638 for (const auto &p : config().participants_order()) {
639 LOG_DBG("Pregenerating participant {}", p);
640 generate_participant(ostr, p);
641 }
642 }
643
645
647
649}
650
651void generator::generate_from_sequences(std::ostream &ostr) const
652{
653 std::vector<eid_t> start_from = find_from_activities();
654
655 // Use this to break out of recurrent loops
656 std::vector<eid_t> visited_participants;
657
658 for (const auto from_id : start_from) {
659 if (model().participants().count(from_id) == 0)
660 continue;
661
662 const auto &from = model().get_participant<model::function>(from_id);
663
664 if (!from.has_value()) {
665 LOG_WARN("Failed to find participant {} for 'from' "
666 "condition");
667 continue;
668 }
669
670 generate_participant(ostr, from_id);
671
672 std::string from_alias = generate_alias(from.value());
673
676
677 // For methods or functions in diagrams where they are
678 // combined into file participants, we need to add an
679 // 'entry' point call to know which method relates to the
680 // first activity for this 'start_from' condition
681 if (from.value().type_name() == "method" ||
682 from.value().type_name() == "objc_method" ||
683 config().combine_free_functions_into_file_participants()) {
684 ostr << "[->" << " " << from_alias << " : "
685 << render_message_name(from.value().message_name(render_mode))
686 << '\n';
687 }
688
689 ostr << "activate " << from_alias << '\n';
690
691 generate_activity(from_id, ostr, visited_participants);
692
693 ostr << "deactivate " << from_alias << '\n';
694 }
695}
696
697std::vector<eid_t> generator::find_from_activities() const
698{
699 std::vector<eid_t> start_from;
700 for (const auto &sf : config().from()) {
701 if (sf.location_type == location_t::function) {
702 bool found{false};
703 for (const auto &[k, v] : model().sequences()) {
704 if (model().participants().count(v.from()) == 0)
705 continue;
706
707 const auto &caller = *model().participants().at(v.from());
708 std::string vfrom = caller.full_name(false);
709 if (sf.location == vfrom) {
710 LOG_DBG("Found sequence diagram start point: {}", k);
711 start_from.push_back(k);
712 found = true;
713 }
714 }
715
716 if (!found) {
717 model().handle_invalid_from_condition(sf);
718 }
719 }
720 }
721
722 return start_from;
723}
724
725std::vector<model::message_chain_t> generator::find_to_message_chains() const
726{
727 std::vector<model::message_chain_t> result;
728
729 for (const auto &to_location : config().to()) {
730 auto to_activity_ids = model().get_to_activity_ids(to_location);
731
732 if (to_activity_ids.empty()) {
733 model().handle_invalid_to_condition(to_location);
734 }
735
736 for (const auto &to_activity_id : to_activity_ids) {
737 std::vector<model::message_chain_t> message_chains_unique =
738 model().get_all_from_to_message_chains(eid_t{}, to_activity_id);
739
740 result.insert(result.end(), message_chains_unique.begin(),
741 message_chains_unique.end());
742 }
743 }
744
745 return result;
746}
747
748std::string generator::render_message_name(const std::string &m) const
749{
750 return util::abbreviate(m, config().message_name_width());
751}
752
753void generator::generate_to_sequences(std::ostream &ostr) const
754{
755 std::vector<model::message_chain_t> message_chains =
757
758 bool first_separator_skipped{false};
759 for (const auto &mc : message_chains) {
760 if (!first_separator_skipped)
761 first_separator_skipped = true;
762 else
763 ostr << "====\n";
764
765 const auto from_activity_id = mc.front().from();
766
767 if (model().participants().count(from_activity_id) == 0)
768 continue;
769
770 const auto &from =
771 model().get_participant<model::function>(from_activity_id);
772
773 if (from.value().type_name() == "method" ||
774 from.value().type_name() == "objc_method" ||
775 config().combine_free_functions_into_file_participants()) {
776 generate_participant(ostr, from_activity_id);
777 ostr << "[->" << " " << generate_alias(from.value()) << " : "
778 << render_message_name(from.value().message_name(
780 << '\n';
781 }
782
783 for (const auto &m : mc) {
784 generate_call(m, ostr);
785 }
786 }
787}
788
789void generator::generate_from_to_sequences(std::ostream &ostr) const
790{
791 for (const auto &ft : config().from_to()) {
792 // First, find the sequence of activities from 'from' location
793 // to 'to' location
794 assert(ft.size() == 2);
795
796 const auto &from_location = ft.front();
797 const auto &to_location = ft.back();
798
799 const auto from_activity_ids =
800 model().get_from_activity_ids(from_location);
801
802 const auto to_activity_ids = model().get_to_activity_ids(to_location);
803
804 if (from_activity_ids.empty()) {
805 model().handle_invalid_from_condition(from_location);
806 }
807
808 if (from_activity_ids.empty() || to_activity_ids.empty()) {
809 model().handle_invalid_to_condition(to_location);
810 }
811
812 bool first_separator_skipped{false};
813
814 for (const auto from_activity_id : from_activity_ids) {
815 if (model().participants().count(from_activity_id) == 0)
816 continue;
817
818 for (const auto to_activity_id : to_activity_ids) {
819 if (model().participants().count(to_activity_id) == 0)
820 continue;
821
822 auto message_chains_unique =
823 model().get_all_from_to_message_chains(
824 from_activity_id, to_activity_id);
825
826 for (const auto &mc : message_chains_unique) {
827 if (!first_separator_skipped)
828 first_separator_skipped = true;
829 else
830 ostr << "====\n";
831
832 const auto &from = model().get_participant<model::function>(
833 from_activity_id);
834
835 if (from.value().type_name() == "method" ||
836 from.value().type_name() == "objc_method" ||
837 config()
838 .combine_free_functions_into_file_participants()) {
839 generate_participant(ostr, from_activity_id);
840 ostr << "[->" << " " << generate_alias(from.value())
841 << " : "
842 << render_message_name(from.value().message_name(
844 << '\n';
845 }
846
847 for (const auto &m : mc) {
848 generate_call(m, ostr);
849 }
850 }
851 }
852 }
853 }
854}
855
858{
859 if (config().generate_method_arguments() ==
862
863 if (config().generate_method_arguments() == config::method_arguments::none)
865
867}
868
869} // namespace clanguml::sequence_diagram::generators::plantuml