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