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