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/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 message = f.message_name(render_mode);
114 }
115 else if (to.value().type_name() == "objc_method") {
116 const auto &f = dynamic_cast<const model::objc_method &>(to.value());
117 message = f.message_name(render_mode);
118 }
119 else if (config().combine_free_functions_into_file_participants()) {
120 if (to.value().type_name() == "function") {
121 const auto &f = dynamic_cast<const model::function &>(to.value());
122
123 message = f.message_name(render_mode);
124
125 if (f.is_cuda_kernel())
126 message = fmt::format("<< CUDA Kernel >><br>{}", message);
127 else if (f.is_cuda_device())
128 message = fmt::format("<< CUDA Device >><br>{}", message);
129 }
130 else if (to.value().type_name() == "function_template") {
131 const auto &f = dynamic_cast<const model::function &>(to.value());
132 message = f.message_name(render_mode);
133
134 if (f.is_cuda_kernel())
135 message = fmt::format("<< CUDA Kernel >><br>{}", message);
136 else if (f.is_cuda_device())
137 message = fmt::format("<< CUDA Device >><br>{}", message);
138 }
139 }
140
141 message = config().simplify_template_type(message);
142
143 const std::string from_alias = generate_alias(from.value());
144 const std::string to_alias = generate_alias(to.value());
145
146 print_debug(m, ostr);
147
149
150 ostr << indent(1) << from_alias << " "
151 << common::generators::mermaid::to_mermaid(message_t::kCall) << " ";
152
153 ostr << to_alias;
154
155 ostr << " : ";
156
158 ostr << "[";
159
160 ostr << message;
161
163 ostr << "]";
164
165 ostr << '\n';
166
167 LOG_DBG("Generated call '{}' from {} [{}] to {} [{}]", message,
168 from.value().full_name(false), m.from(), to.value().full_name(false),
169 m.to());
170}
171
172void generator::generate_return(const message &m, std::ostream &ostr) const
173{
174 // Add return activity only for messages between different actors and
175 // only if the return type is different than void
176 const auto &from = model().get_participant<model::participant>(m.from());
177 const auto &to = model().get_participant<model::function>(m.to());
178 if ((m.from() != m.to()) && !to.value().is_void()) {
179 const std::string from_alias = generate_alias(from.value());
180
181 const std::string to_alias = generate_alias(to.value());
182
183 ostr << indent(1) << to_alias << " "
184 << common::generators::mermaid::to_mermaid(message_t::kReturn)
185 << " " << from_alias << " : ";
186
187 if (config().generate_return_types()) {
188 ostr << m.return_type();
189 }
190
191 ostr << '\n';
192 }
193}
194
196 eid_t activity_id, std::ostream &ostr, std::vector<eid_t> &visited) const
197{
198 const auto &a = model().get_activity(activity_id);
199
200 const auto [it, inserted] = generated_activities_.emplace(activity_id);
201
202 if (config().fold_repeated_activities() && !inserted &&
203 !a.messages().empty()) {
204 const auto &p =
205 model().get_participant<model::participant>(activity_id);
206
207 if (p.has_value()) {
208 ostr << indent(1) << "Note over " << generate_alias(p.value())
209 << " : *\n";
210 }
211
212 return;
213 }
214
215 for (const auto &m : a.messages()) {
216 if (m.in_static_declaration_context()) {
218 continue;
219
221 }
222
223 if (m.type() == message_t::kCall) {
224 const auto &to =
225 model().get_participant<model::participant>(m.to());
226
227 visited.push_back(m.from());
228
229 LOG_DBG("Generating message [{}] --> [{}]", m.from(), m.to());
230
231 generate_call(m, ostr);
232
233 std::string to_alias = generate_alias(to.value());
234
235 ostr << indent(1) << "activate " << to_alias << '\n';
236
237 if (model().sequences().find(m.to()) != model().sequences().end()) {
238 if (std::find(visited.begin(), visited.end(), m.to()) ==
239 visited
240 .end()) { // break infinite recursion on recursive calls
241 LOG_DBG("Creating activity {} --> {} - missing sequence {}",
242 m.from(), m.to(), m.to());
243 generate_activity(m.to(), ostr, visited);
244 }
245 }
246 else
247 LOG_DBG("Skipping activity {} --> {} - missing sequence {}",
248 m.from(), m.to(), m.to());
249
250 generate_return(m, ostr);
251
252 ostr << indent(1) << "deactivate " << to_alias << '\n';
253
254 visited.pop_back();
255 }
256 else if (m.type() == message_t::kIf) {
257 print_debug(m, ostr);
259 ostr << indent(1) << "alt";
260 if (const auto &text = m.condition_text(); text.has_value())
261 ostr << " " << render_message_text(text.value());
262 ostr << '\n';
263 }
264 else if (m.type() == message_t::kElseIf) {
265 print_debug(m, ostr);
266 ostr << indent(1) << "else";
267 if (const auto &text = m.condition_text(); text.has_value())
268 ostr << " " << render_message_text(text.value());
269 ostr << '\n';
270 }
271 else if (m.type() == message_t::kElse) {
272 print_debug(m, ostr);
273 ostr << indent(1) << "else\n";
274 }
275 else if (m.type() == message_t::kIfEnd) {
276 ostr << indent(1) << "end\n";
277 }
278 else if (m.type() == message_t::kWhile) {
279 print_debug(m, ostr);
281 ostr << indent(1) << "loop";
282 if (const auto &text = m.condition_text(); text.has_value())
283 ostr << " " << render_message_text(text.value());
284 ostr << '\n';
285 }
286 else if (m.type() == message_t::kWhileEnd) {
287 ostr << indent(1) << "end\n";
288 }
289 else if (m.type() == message_t::kFor) {
290 print_debug(m, ostr);
292 ostr << indent(1) << "loop";
293 if (const auto &text = m.condition_text(); text.has_value())
294 ostr << " " << render_message_text(text.value());
295 ostr << '\n';
296 }
297 else if (m.type() == message_t::kForEnd) {
298 ostr << "end\n";
299 }
300 else if (m.type() == message_t::kDo) {
301 print_debug(m, ostr);
303 ostr << indent(1) << "loop";
304 if (const auto &text = m.condition_text(); text.has_value())
305 ostr << " " << render_message_text(text.value());
306 ostr << '\n';
307 }
308 else if (m.type() == message_t::kDoEnd) {
309 ostr << indent(1) << "end\n";
310 }
311 else if (m.type() == message_t::kTry) {
312 print_debug(m, ostr);
314 ostr << indent(1) << "critical\n";
315 }
316 else if (m.type() == message_t::kCatch) {
317 print_debug(m, ostr);
318 ostr << indent(1) << "option "
319 << render_message_text(m.message_name()) << '\n';
320 }
321 else if (m.type() == message_t::kTryEnd) {
322 print_debug(m, ostr);
323 ostr << indent(1) << "end\n";
324 }
325 else if (m.type() == message_t::kSwitch) {
326 print_debug(m, ostr);
328 ostr << indent(1) << "alt\n";
329 }
330 else if (m.type() == message_t::kCase) {
331 print_debug(m, ostr);
332 ostr << indent(1) << "else "
333 << render_message_text(m.message_name()) << '\n';
334 }
335 else if (m.type() == message_t::kSwitchEnd) {
336 ostr << indent(1) << "end\n";
337 }
338 else if (m.type() == message_t::kConditional) {
339 print_debug(m, ostr);
341 ostr << indent(1) << "alt";
342 if (const auto &text = m.condition_text(); text.has_value())
343 ostr << " " << render_message_text(text.value());
344 ostr << '\n';
345 }
346 else if (m.type() == message_t::kConditionalElse) {
347 print_debug(m, ostr);
348 ostr << indent(1) << "else\n";
349 }
350 else if (m.type() == message_t::kConditionalEnd) {
351 ostr << indent(1) << "end\n";
352 }
353 }
354}
355
357 std::ostream &ostr, const std::string &name) const
358{
359 auto p = model().get(name);
360
361 if (!p.has_value()) {
362 LOG_WARN("Cannot find participant {} from `participants_order` option",
363 name);
364 return;
365 }
366
367 generate_participant(ostr, p.value().id(), true);
368}
369
371 std::ostream &ostr, eid_t id, bool force) const
372{
373 eid_t participant_id{};
374
375 if (!force) {
376 for (const auto pid : model().active_participants()) {
377 if (pid == id) {
378 participant_id = pid;
379 break;
380 }
381 }
382 }
383 else
384 participant_id = id;
385
386 if (participant_id == 0)
387 return;
388
389 if (is_participant_generated(participant_id))
390 return;
391
392 const auto &participant =
393 model().get_participant<model::participant>(participant_id).value();
394
395 if (participant.type_name() == "method") {
396 const auto class_id =
397 model()
398 .get_participant<model::method>(participant_id)
399 .value()
400 .class_id();
401
402 if (is_participant_generated(class_id))
403 return;
404
405 const auto &class_participant =
406 model().get_participant<model::participant>(class_id).value();
407
408 print_debug(class_participant, ostr);
409
410 auto participant_name =
411 config().using_namespace().relative(config().simplify_template_type(
412 display_name_adapter(class_participant).full_name(false)));
414
415 ostr << indent(1) << "participant " << class_participant.alias()
416 << " as " << participant_name;
417
418 ostr << '\n';
419
420 generated_participants_.emplace(class_id);
421 }
422 else if (participant.type_name() == "objc_method") {
423 const auto class_id =
424 model()
425 .get_participant<model::objc_method>(participant_id)
426 .value()
427 .class_id();
428
429 if (is_participant_generated(class_id))
430 return;
431
432 const auto &class_participant =
433 model().get_participant<model::participant>(class_id).value();
434
435 print_debug(class_participant, ostr);
436
437 auto participant_name =
438 config().using_namespace().relative(config().simplify_template_type(
439 display_name_adapter(class_participant).full_name(false)));
441
442 ostr << indent(1) << "participant " << class_participant.alias()
443 << " as " << "<< ObjC Interface >><br>" << participant_name;
444
445 ostr << '\n';
446
447 generated_participants_.emplace(class_id);
448 }
449 else if ((participant.type_name() == "function" ||
450 participant.type_name() == "function_template") &&
451 config().combine_free_functions_into_file_participants()) {
452 // Create a single participant for all functions declared in a
453 // single file
454 const auto &f =
455 model().get_participant<model::function>(participant_id).value();
456
457 const auto &file_path = f.file();
458
459 assert(!file_path.empty());
460
461 const auto file_id = common::to_id(file_path);
462
463 if (is_participant_generated(file_id))
464 return;
465
466 auto participant_name = util::path_to_url(std::filesystem::relative(
467 std::filesystem::path{file_path}, config().root_directory())
468 .string());
469
470 ostr << indent(1) << "participant "
471 << fmt::format("C_{:022}", file_id.value()) << " as "
472 << participant_name;
473 ostr << '\n';
474
475 generated_participants_.emplace(file_id);
476 }
477 else {
478 print_debug(participant, ostr);
479
480 auto participant_name =
481 config().using_namespace().relative(config().simplify_template_type(
482 display_name_adapter(participant).full_name(false)));
484
485 ostr << indent(1) << "participant " << participant.alias() << " as ";
486
487 if (participant.type_name() == "function" ||
488 participant.type_name() == "function_template") {
489 const auto &f =
490 model()
491 .get_participant<model::function>(participant_id)
492 .value();
493
494 if (f.is_cuda_kernel())
495 ostr << "<< CUDA Kernel >><br>";
496 else if (f.is_cuda_device())
497 ostr << "<< CUDA Device >><br>";
498 }
499
500 ostr << participant_name;
501 ostr << '\n';
502
503 generated_participants_.emplace(participant_id);
504 }
505}
506
508{
509 return std::find(generated_participants_.begin(),
511 id) != generated_participants_.end();
512}
513
515 const model::participant &participant) const
516{
517 if ((participant.type_name() == "function" ||
518 participant.type_name() == "function_template") &&
519 config().combine_free_functions_into_file_participants()) {
520 const auto file_id = common::to_id(participant.file());
521
522 return fmt::format("C_{:022}", file_id.value());
523 }
524
525 return participant.alias();
526}
527
528void generator::generate_diagram(std::ostream &ostr) const
529{
530 model().print();
531
532 if (config().participants_order.has_value) {
533 for (const auto &p : config().participants_order()) {
534 LOG_DBG("Pregenerating participant {}", p);
535 generate_participant(ostr, p);
536 }
537 }
538
539 bool star_participant_generated{false};
540
541 generate_from_to_sequences(ostr, star_participant_generated);
542
544
546}
547
548void generator::generate_from_sequences(std::ostream &ostr) const
549{
550 std::vector<eid_t> start_from = find_from_activities();
551
552 // Use this to break out of recurrent loops
553 std::vector<eid_t> visited_participants;
554 for (const auto from_id : start_from) {
555 if (model().participants().count(from_id) == 0)
556 continue;
557
558 const auto &from = model().get_participant<model::function>(from_id);
559
560 if (!from.has_value()) {
561 LOG_WARN("Failed to find participant {} for 'from' "
562 "condition");
563 continue;
564 }
565
566 generate_participant(ostr, from_id);
567
568 std::string from_alias = generate_alias(from.value());
569
572
573 // For methods or functions in diagrams where they are combined into
574 // file participants, we need to add an 'entry' point call to know
575 // which method relates to the first activity for this 'start_from'
576 // condition
577 if (from.value().type_name() == "method" ||
578 config().combine_free_functions_into_file_participants()) {
579 ostr << indent(1) << "* "
580 << common::generators::mermaid::to_mermaid(message_t::kCall)
581 << " " << from_alias << " : "
582 << from.value().message_name(render_mode) << '\n';
583 }
584
585 ostr << indent(1) << "activate " << from_alias << '\n';
586
587 generate_activity(from_id, ostr, visited_participants);
588
589 if (from.value().type_name() == "method" ||
590 config().combine_free_functions_into_file_participants()) {
591
592 if (!from.value().is_void()) {
593 ostr << indent(1) << from_alias << " "
595 message_t::kReturn)
596 << " *" << " : ";
597
598 if (config().generate_return_types())
599 ostr << from.value().return_type();
600
601 ostr << '\n';
602 }
603 }
604
605 ostr << indent(1) << "deactivate " << from_alias << '\n';
606 }
607}
608
609std::vector<model::message_chain_t> generator::find_to_message_chains() const
610{
611 std::vector<model::message_chain_t> result;
612
613 for (const auto &to_location : config().to()) {
614 auto to_activity_ids = model().get_to_activity_ids(to_location);
615
616 if (to_activity_ids.empty()) {
617 LOG_WARN("Failed to find participant matching '{}' for "
618 "'to' condition: ",
619 to_location.location.to_string());
620 }
621
622 for (const auto &to_activity_id : to_activity_ids) {
623 std::vector<model::message_chain_t> message_chains_unique =
624 model().get_all_from_to_message_chains(eid_t{}, to_activity_id);
625
626 result.insert(result.end(), message_chains_unique.begin(),
627 message_chains_unique.end());
628 }
629 }
630
631 return result;
632}
633
634void generator::generate_to_sequences(std::ostream &ostr) const
635{
636 std::vector<model::message_chain_t> message_chains =
638
639 for (const auto &mc : message_chains) {
640 const auto from_activity_id = mc.front().from();
641
642 if (model().participants().count(from_activity_id) == 0)
643 continue;
644
645 const auto &from =
646 model().get_participant<model::function>(from_activity_id);
647
648 if (from.value().type_name() == "method" ||
649 config().combine_free_functions_into_file_participants()) {
650 generate_participant(ostr, from_activity_id);
651 ostr << indent(1) << "* "
652 << common::generators::mermaid::to_mermaid(message_t::kCall)
653 << " " << generate_alias(from.value()) << " : "
654 << from.value().message_name(
656 << '\n';
657 }
658
659 for (const auto &m : mc) {
660 generate_call(m, ostr);
661 }
662 }
663}
664
666 std::ostream &ostr, bool star_participant_generated) const
667{
668 for (const auto &ft : config().from_to()) {
669 // First, find the sequence of activities from 'from' location
670 // to 'to' location
671 assert(ft.size() == 2);
672
673 const auto &from_location = ft.front();
674 const auto &to_location = ft.back();
675
676 auto from_activity_ids = model().get_from_activity_ids(from_location);
677 auto to_activity_ids = model().get_to_activity_ids(to_location);
678
679 if (from_activity_ids.empty()) {
681 model().name(),
682 fmt::format("Failed to find participant matching '{}' for "
683 "'from' condition: ",
684 from_location.location.to_string()));
685 }
686
687 if (from_activity_ids.empty() || to_activity_ids.empty()) {
689 model().name(),
690 fmt::format("Failed to find participant matching '{}' for "
691 "'to' condition: ",
692 to_location.location.to_string()));
693 }
694
695 for (const auto from_activity_id : from_activity_ids) {
696 if (model().participants().count(from_activity_id) == 0)
697 continue;
698
699 for (const auto to_activity_id : to_activity_ids) {
700 if (model().participants().count(to_activity_id) == 0)
701 continue;
702
703 auto message_chains_unique =
704 model().get_all_from_to_message_chains(
705 from_activity_id, to_activity_id);
706
707 for (const auto &mc : message_chains_unique) {
708 const auto &from = model().get_participant<model::function>(
709 from_activity_id);
710
711 if (from.value().type_name() == "method" ||
712 config()
713 .combine_free_functions_into_file_participants()) {
714 if (!star_participant_generated) {
715 ostr << indent(1) << "participant *\n";
716 star_participant_generated = true;
717 }
718 generate_participant(ostr, from_activity_id);
719 ostr << indent(1) << "* "
721 message_t::kCall)
722 << " " << generate_alias(from.value()) << " : "
723 << from.value().message_name(
725 << '\n';
726 }
727
728 for (const auto &m : mc) {
729 generate_call(m, ostr);
730 }
731 }
732 }
733 }
734 }
735}
736
737std::vector<eid_t> generator::find_from_activities() const
738{
739 std::vector<eid_t> start_from;
740 for (const auto &sf : config().from()) {
741 if (sf.location_type == location_t::function) {
742 bool found{false};
743 for (const auto &[k, v] : model().sequences()) {
744 if (model().participants().count(v.from()) == 0)
745 continue;
746
747 const auto &caller = *model().participants().at(v.from());
748 std::string vfrom = caller.full_name(false);
749 if (sf.location == vfrom) {
750 LOG_DBG("Found sequence diagram start point: {}", k);
751 start_from.push_back(k);
752 found = true;
753 }
754 }
755
756 if (!found)
758 model().name(),
759 fmt::format("Failed to find participant matching '{}' for "
760 "'from' condition: ",
761 sf.location.to_string()));
762 }
763 }
764
765 return start_from;
766}
767
770{
771 if (config().generate_method_arguments() ==
774
775 if (config().generate_method_arguments() == config::method_arguments::none)
777
779}
780
781} // namespace clanguml::sequence_diagram::generators::mermaid