0.5.4
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-2024 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
27using namespace clanguml::util;
28
30
31std::string render_participant_name(std::string name)
32{
33 util::replace_all(name, "##", "::");
34
35 return name;
36}
37
38std::string render_message_text(std::string text)
39{
40 util::replace_all(text, ";", "&#59;");
41
42 return text;
43}
44
48{
49}
50
51void generator::generate_diagram_type(std::ostream &ostr) const
52{
53 ostr << "sequenceDiagram\n";
54}
55
57 std::ostream &ostr, const model::message &m) const
58{
59 const auto &from = model().get_participant<model::participant>(m.from());
60 if (!from)
61 return;
62
63 bool comment_generated_from_note_decorators{false};
64 for (const auto &decorator : m.decorators()) {
65 auto note = std::dynamic_pointer_cast<decorators::note>(decorator);
66 if (note && note->applies_to_diagram(config().name)) {
67 comment_generated_from_note_decorators = true;
68
69 ostr << indent(1) << "note over " << generate_alias(from.value())
70 << ": ";
71
72 auto formatted_message = util::format_message_comment(
73 note->text, config().message_comment_width());
74
75 util::replace_all(formatted_message, "\n", "<br/>");
76 ostr << formatted_message << '\n';
77 }
78 }
79
80 if (comment_generated_from_note_decorators)
81 return;
82
83 if (const auto &cmt = m.comment(); config().generate_message_comments() &&
84 cmt.has_value() &&
85 generated_comment_ids_.emplace(cmt.value().at("id")).second) {
86
87 ostr << indent(1) << "note over " << generate_alias(from.value())
88 << ": ";
89
90 auto formatted_message = util::format_message_comment(
91 cmt.value().at("comment"), config().message_comment_width());
92
93 util::replace_all(formatted_message, "\n", "<br/>");
94
95 ostr << formatted_message << '\n';
96 }
97}
98
99void generator::generate_call(const message &m, std::ostream &ostr) const
100{
101 const auto &from = model().get_participant<model::participant>(m.from());
102 const auto &to = model().get_participant<model::participant>(m.to());
103
104 if (!from || !to) {
105 LOG_DBG("Skipping empty call from '{}' to '{}'", m.from(), m.to());
106 return;
107 }
108
109 generate_participant(ostr, m.from());
110 generate_participant(ostr, m.to());
111
112 std::string message;
113
116
117 if (to.value().type_name() == "method") {
118 const auto &f = dynamic_cast<const model::method &>(to.value());
119 message = f.message_name(render_mode);
120 }
121 else if (config().combine_free_functions_into_file_participants()) {
122 if (to.value().type_name() == "function") {
123 const auto &f = dynamic_cast<const model::function &>(to.value());
124
125 message = f.message_name(render_mode);
126
127 if (f.is_cuda_kernel())
128 message = fmt::format("<< CUDA Kernel >><br>{}", message);
129 else if (f.is_cuda_device())
130 message = fmt::format("<< CUDA Device >><br>{}", message);
131 }
132 else if (to.value().type_name() == "function_template") {
133 const auto &f = dynamic_cast<const model::function &>(to.value());
134 message = f.message_name(render_mode);
135
136 if (f.is_cuda_kernel())
137 message = fmt::format("<< CUDA Kernel >><br>{}", message);
138 else if (f.is_cuda_device())
139 message = fmt::format("<< CUDA Device >><br>{}", message);
140 }
141 }
142
143 message = config().simplify_template_type(message);
144
145 const std::string from_alias = generate_alias(from.value());
146 const std::string to_alias = generate_alias(to.value());
147
148 print_debug(m, ostr);
149
151
152 ostr << indent(1) << from_alias << " "
153 << common::generators::mermaid::to_mermaid(message_t::kCall) << " ";
154
155 ostr << to_alias;
156
157 ostr << " : ";
158
160 ostr << "[";
161
162 ostr << message;
163
165 ostr << "]";
166
167 ostr << '\n';
168
169 LOG_DBG("Generated call '{}' from {} [{}] to {} [{}]", message,
170 from.value().full_name(false), m.from(), to.value().full_name(false),
171 m.to());
172}
173
174void generator::generate_return(const message &m, std::ostream &ostr) const
175{
176 // Add return activity only for messages between different actors and
177 // only if the return type is different than void
178 const auto &from = model().get_participant<model::participant>(m.from());
179 const auto &to = model().get_participant<model::function>(m.to());
180 if ((m.from() != m.to()) && !to.value().is_void()) {
181 const std::string from_alias = generate_alias(from.value());
182
183 const std::string to_alias = generate_alias(to.value());
184
185 ostr << indent(1) << to_alias << " "
186 << common::generators::mermaid::to_mermaid(message_t::kReturn)
187 << " " << from_alias << " : ";
188
189 if (config().generate_return_types()) {
190 ostr << m.return_type();
191 }
192
193 ostr << '\n';
194 }
195}
196
198 const activity &a, std::ostream &ostr, std::vector<eid_t> &visited) const
199{
200 for (const auto &m : a.messages()) {
201 if (m.in_static_declaration_context()) {
203 continue;
204
206 }
207
208 if (m.type() == message_t::kCall) {
209 const auto &to =
210 model().get_participant<model::participant>(m.to());
211
212 visited.push_back(m.from());
213
214 LOG_DBG("Generating message [{}] --> [{}]", m.from(), m.to());
215
216 generate_call(m, ostr);
217
218 std::string to_alias = generate_alias(to.value());
219
220 ostr << indent(1) << "activate " << to_alias << '\n';
221
222 if (model().sequences().find(m.to()) != model().sequences().end()) {
223 if (std::find(visited.begin(), visited.end(), m.to()) ==
224 visited
225 .end()) { // break infinite recursion on recursive calls
226 LOG_DBG("Creating activity {} --> {} - missing sequence {}",
227 m.from(), m.to(), m.to());
229 model().get_activity(m.to()), ostr, visited);
230 }
231 }
232 else
233 LOG_DBG("Skipping activity {} --> {} - missing sequence {}",
234 m.from(), m.to(), m.to());
235
236 generate_return(m, ostr);
237
238 ostr << indent(1) << "deactivate " << to_alias << '\n';
239
240 visited.pop_back();
241 }
242 else if (m.type() == message_t::kIf) {
243 print_debug(m, ostr);
245 ostr << indent(1) << "alt";
246 if (const auto &text = m.condition_text(); text.has_value())
247 ostr << " " << render_message_text(text.value());
248 ostr << '\n';
249 }
250 else if (m.type() == message_t::kElseIf) {
251 print_debug(m, ostr);
252 ostr << indent(1) << "else";
253 if (const auto &text = m.condition_text(); text.has_value())
254 ostr << " " << render_message_text(text.value());
255 ostr << '\n';
256 }
257 else if (m.type() == message_t::kElse) {
258 print_debug(m, ostr);
259 ostr << indent(1) << "else\n";
260 }
261 else if (m.type() == message_t::kIfEnd) {
262 ostr << indent(1) << "end\n";
263 }
264 else if (m.type() == message_t::kWhile) {
265 print_debug(m, ostr);
267 ostr << indent(1) << "loop";
268 if (const auto &text = m.condition_text(); text.has_value())
269 ostr << " " << render_message_text(text.value());
270 ostr << '\n';
271 }
272 else if (m.type() == message_t::kWhileEnd) {
273 ostr << indent(1) << "end\n";
274 }
275 else if (m.type() == message_t::kFor) {
276 print_debug(m, ostr);
278 ostr << indent(1) << "loop";
279 if (const auto &text = m.condition_text(); text.has_value())
280 ostr << " " << render_message_text(text.value());
281 ostr << '\n';
282 }
283 else if (m.type() == message_t::kForEnd) {
284 ostr << "end\n";
285 }
286 else if (m.type() == message_t::kDo) {
287 print_debug(m, ostr);
289 ostr << indent(1) << "loop";
290 if (const auto &text = m.condition_text(); text.has_value())
291 ostr << " " << render_message_text(text.value());
292 ostr << '\n';
293 }
294 else if (m.type() == message_t::kDoEnd) {
295 ostr << indent(1) << "end\n";
296 }
297 else if (m.type() == message_t::kTry) {
298 print_debug(m, ostr);
300 ostr << indent(1) << "critical\n";
301 }
302 else if (m.type() == message_t::kCatch) {
303 print_debug(m, ostr);
304 ostr << indent(1) << "option "
305 << render_message_text(m.message_name()) << '\n';
306 }
307 else if (m.type() == message_t::kTryEnd) {
308 print_debug(m, ostr);
309 ostr << indent(1) << "end\n";
310 }
311 else if (m.type() == message_t::kSwitch) {
312 print_debug(m, ostr);
314 ostr << indent(1) << "alt\n";
315 }
316 else if (m.type() == message_t::kCase) {
317 print_debug(m, ostr);
318 ostr << indent(1) << "else "
319 << render_message_text(m.message_name()) << '\n';
320 }
321 else if (m.type() == message_t::kSwitchEnd) {
322 ostr << indent(1) << "end\n";
323 }
324 else if (m.type() == message_t::kConditional) {
325 print_debug(m, ostr);
327 ostr << indent(1) << "alt";
328 if (const auto &text = m.condition_text(); text.has_value())
329 ostr << " " << render_message_text(text.value());
330 ostr << '\n';
331 }
332 else if (m.type() == message_t::kConditionalElse) {
333 print_debug(m, ostr);
334 ostr << indent(1) << "else\n";
335 }
336 else if (m.type() == message_t::kConditionalEnd) {
337 ostr << indent(1) << "end\n";
338 }
339 }
340}
341
343 std::ostream &ostr, const std::string &name) const
344{
345 auto p = model().get(name);
346
347 if (!p.has_value()) {
348 LOG_WARN("Cannot find participant {} from `participants_order` option",
349 name);
350 return;
351 }
352
353 generate_participant(ostr, p.value().id(), true);
354}
355
357 std::ostream &ostr, eid_t id, bool force) const
358{
359 eid_t participant_id{};
360
361 if (!force) {
362 for (const auto pid : model().active_participants()) {
363 if (pid == id) {
364 participant_id = pid;
365 break;
366 }
367 }
368 }
369 else
370 participant_id = id;
371
372 if (participant_id == 0)
373 return;
374
375 if (is_participant_generated(participant_id))
376 return;
377
378 const auto &participant =
379 model().get_participant<model::participant>(participant_id).value();
380
381 if (participant.type_name() == "method") {
382 const auto class_id =
383 model()
384 .get_participant<model::method>(participant_id)
385 .value()
386 .class_id();
387
388 if (is_participant_generated(class_id))
389 return;
390
391 const auto &class_participant =
392 model().get_participant<model::participant>(class_id).value();
393
394 print_debug(class_participant, ostr);
395
396 auto participant_name =
397 config().using_namespace().relative(config().simplify_template_type(
398 class_participant.full_name(false)));
400
401 ostr << indent(1) << "participant " << class_participant.alias()
402 << " as " << render_participant_name(participant_name);
403
404 ostr << '\n';
405
406 generated_participants_.emplace(class_id);
407 }
408 else if ((participant.type_name() == "function" ||
409 participant.type_name() == "function_template") &&
410 config().combine_free_functions_into_file_participants()) {
411 // Create a single participant for all functions declared in a
412 // single file
413 const auto &f =
414 model().get_participant<model::function>(participant_id).value();
415
416 const auto &file_path = f.file();
417
418 assert(!file_path.empty());
419
420 const auto file_id = common::to_id(file_path);
421
422 if (is_participant_generated(file_id))
423 return;
424
425 auto participant_name = util::path_to_url(std::filesystem::relative(
426 std::filesystem::path{file_path}, config().root_directory())
427 .string());
428
429 ostr << indent(1) << "participant "
430 << fmt::format("C_{:022}", file_id.value()) << " as "
431 << render_participant_name(participant_name);
432 ostr << '\n';
433
434 generated_participants_.emplace(file_id);
435 }
436 else {
437 print_debug(participant, ostr);
438
439 auto participant_name = config().using_namespace().relative(
440 config().simplify_template_type(participant.full_name(false)));
442
443 ostr << indent(1) << "participant " << participant.alias() << " as ";
444
445 if (participant.type_name() == "function" ||
446 participant.type_name() == "function_template") {
447 const auto &f =
448 model()
449 .get_participant<model::function>(participant_id)
450 .value();
451
452 if (f.is_cuda_kernel())
453 ostr << "<< CUDA Kernel >><br>";
454 else if (f.is_cuda_device())
455 ostr << "<< CUDA Device >><br>";
456 }
457
458 ostr << render_participant_name(participant_name);
459 ostr << '\n';
460
461 generated_participants_.emplace(participant_id);
462 }
463}
464
466{
467 return std::find(generated_participants_.begin(),
469 id) != generated_participants_.end();
470}
471
473 const model::participant &participant) const
474{
475 if ((participant.type_name() == "function" ||
476 participant.type_name() == "function_template") &&
477 config().combine_free_functions_into_file_participants()) {
478 const auto file_id = common::to_id(participant.file());
479
480 return fmt::format("C_{:022}", file_id.value());
481 }
482
483 return participant.alias();
484}
485
486void generator::generate_diagram(std::ostream &ostr) const
487{
488 model().print();
489
490 if (config().participants_order.has_value) {
491 for (const auto &p : config().participants_order()) {
492 LOG_DBG("Pregenerating participant {}", p);
493 generate_participant(ostr, p);
494 }
495 }
496
497 bool star_participant_generated{false};
498
499 for (const auto &ft : config().from_to()) {
500 // First, find the sequence of activities from 'from' location
501 // to 'to' location
502 assert(ft.size() == 2);
503
504 const auto &from_location = ft.front();
505 const auto &to_location = ft.back();
506
507 auto from_activity_id = model().get_from_activity_id(from_location);
508 auto to_activity_id = model().get_to_activity_id(to_location);
509
510 if (!from_activity_id || !to_activity_id)
511 continue;
512
513 if (model().participants().count(*from_activity_id) == 0)
514 continue;
515
516 if (model().participants().count(*to_activity_id) == 0)
517 continue;
518
519 auto message_chains_unique = model().get_all_from_to_message_chains(
520 *from_activity_id, *to_activity_id);
521
522 for (const auto &mc : message_chains_unique) {
523 const auto &from =
524 model().get_participant<model::function>(*from_activity_id);
525
526 if (from.value().type_name() == "method" ||
527 config().combine_free_functions_into_file_participants()) {
528 if (!star_participant_generated) {
529 ostr << indent(1) << "participant *\n";
530 star_participant_generated = true;
531 }
532 generate_participant(ostr, *from_activity_id);
533 ostr << indent(1) << "* "
535 message_t::kCall)
536 << " " << generate_alias(from.value()) << " : "
537 << from.value().message_name(
539 << '\n';
540 }
541
542 for (const auto &m : mc) {
543 generate_call(m, ostr);
544 }
545 }
546 }
547
548 for (const auto &to_location : config().to()) {
549 auto to_activity_id = model().get_to_activity_id(to_location);
550
551 if (!to_activity_id)
552 continue;
553
554 auto message_chains_unique =
555 model().get_all_from_to_message_chains(eid_t{}, *to_activity_id);
556
557 for (const auto &mc : message_chains_unique) {
558 const auto from_activity_id = mc.front().from();
559
560 if (model().participants().count(from_activity_id) == 0)
561 continue;
562
563 const auto &from =
564 model().get_participant<model::function>(from_activity_id);
565
566 if (from.value().type_name() == "method" ||
567 config().combine_free_functions_into_file_participants()) {
568 generate_participant(ostr, from_activity_id);
569 ostr << indent(1) << "* "
571 message_t::kCall)
572 << " " << generate_alias(from.value()) << " : "
573 << from.value().message_name(
575 << '\n';
576 }
577
578 for (const auto &m : mc) {
579 generate_call(m, ostr);
580 }
581 }
582 }
583
584 for (const auto &sf : config().from()) {
585 if (sf.location_type == location_t::function) {
586 eid_t start_from{};
587 for (const auto &[k, v] : model().sequences()) {
588 if (model().participants().count(v.from()) == 0)
589 continue;
590
591 const auto &caller = *model().participants().at(v.from());
592 std::string vfrom = caller.full_name(false);
593 if (vfrom == sf.location) {
594 LOG_DBG("Found sequence diagram start point: {}", k);
595 start_from = k;
596 break;
597 }
598 }
599
600 if (start_from == 0) {
601 LOG_WARN("Failed to find participant with {} for start_from "
602 "condition",
603 sf.location);
604 continue;
605 }
606
607 // Use this to break out of recurrent loops
608 std::vector<eid_t> visited_participants;
609
610 if (model().participants().count(start_from) == 0)
611 continue;
612
613 const auto &from =
614 model().get_participant<model::function>(start_from);
615
616 if (!from.has_value()) {
617 LOG_WARN("Failed to find participant {} for start_from "
618 "condition",
619 sf.location);
620 continue;
621 }
622
623 generate_participant(ostr, start_from);
624
625 std::string from_alias = generate_alias(from.value());
626
629
630 // For methods or functions in diagrams where they are combined into
631 // file participants, we need to add an 'entry' point call to know
632 // which method relates to the first activity for this 'start_from'
633 // condition
634 if (from.value().type_name() == "method" ||
635 config().combine_free_functions_into_file_participants()) {
636 ostr << indent(1) << "* "
638 message_t::kCall)
639 << " " << from_alias << " : "
640 << from.value().message_name(render_mode) << '\n';
641 }
642
643 ostr << indent(1) << "activate " << from_alias << '\n';
644
646 model().get_activity(start_from), ostr, visited_participants);
647
648 if (from.value().type_name() == "method" ||
649 config().combine_free_functions_into_file_participants()) {
650
651 if (!from.value().is_void()) {
652 ostr << indent(1) << from_alias << " "
654 message_t::kReturn)
655 << " *"
656 << " : ";
657
658 if (config().generate_return_types())
659 ostr << from.value().return_type();
660
661 ostr << '\n';
662 }
663 }
664
665 ostr << indent(1) << "deactivate " << from_alias << '\n';
666 }
667 else {
668 // TODO: Add support for other sequence start location types
669 continue;
670 }
671 }
672}
673
676{
677 if (config().generate_method_arguments() ==
680
681 if (config().generate_method_arguments() == config::method_arguments::none)
683
685}
686
687} // namespace clanguml::sequence_diagram::generators::mermaid