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