0.5.4
C++ to UML diagram generator based on Clang
Loading...
Searching...
No Matches
class_diagram_generator.cc
Go to the documentation of this file.
1/**
2 * @file src/class_diagram/generators/plantuml/class_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
21#include "util/error.h"
22
23#include <inja/inja.hpp>
24
26
29 , together_group_stack_{!config.generate_packages()}
30{
31}
32
34 std::ostream &ostr, const class_diagram::model::class_element &e) const
35{
36 if (e.file_relative().empty())
37 return;
38
39 auto context = element_context(e);
40
41 auto maybe_link_pattern = get_link_pattern(e);
42 if (maybe_link_pattern) {
43 const auto &[link_prefix, link_pattern] = *maybe_link_pattern;
44 auto ec = element_context(e);
46
47 ostr << " [[[";
48 ostr << env().render(std::string_view{link_pattern}, context);
49 }
50
51 auto maybe_tooltip_pattern = get_tooltip_pattern(e);
52
53 if (maybe_tooltip_pattern) {
54 const auto &[tooltip_prefix, tooltip_pattern] = *maybe_tooltip_pattern;
55 auto ec = element_context(e);
57 ostr << "{";
58 ostr << env().render(std::string_view{tooltip_pattern}, ec);
59 ostr << "}";
60 }
61 ostr << "]]]";
62}
63
64std::string generator::render_name(std::string name) const
65{
66 util::replace_all(name, "##", "::");
67
68 return name;
69}
70
71void generator::generate_alias(const class_ &c, std::ostream &ostr) const
72{
73 std::string class_type{"class"};
74 if (c.is_abstract())
75 class_type = "abstract";
76
77 std::string full_name;
78 if (!config().generate_fully_qualified_name())
79 full_name = c.full_name_no_ns();
80 else
81 full_name = c.full_name();
82
83 assert(!full_name.empty());
84
85 print_debug(c, ostr);
86
87 ostr << class_type << " \""
88 << config().simplify_template_type(render_name(full_name));
89
90 ostr << "\" as " << c.alias() << '\n';
91
92 // Register the added alias
93 m_generated_aliases.emplace(c.alias());
94}
95
96void generator::generate_alias(const enum_ &e, std::ostream &ostr) const
97{
98 print_debug(e, ostr);
99
100 if (!config().generate_fully_qualified_name())
101 ostr << "enum"
102 << " \"" << e.name();
103 else
104 ostr << "enum"
105 << " \"" << render_name(e.full_name());
106
107 ostr << "\" as " << e.alias() << '\n';
108
109 // Register the added alias
110 m_generated_aliases.emplace(e.alias());
111}
112
113void generator::generate_alias(const concept_ &c, std::ostream &ostr) const
114{
115 print_debug(c, ostr);
116
117 if (!config().generate_fully_qualified_name())
118 ostr << "class"
119 << " \"" << c.full_name_no_ns();
120 else
121 ostr << "class"
122 << " \"" << render_name(c.full_name());
123
124 ostr << "\" as " << c.alias() << '\n';
125
126 // Register the added alias
127 m_generated_aliases.emplace(c.alias());
128}
129
130void generator::generate(const class_ &c, std::ostream &ostr) const
131{
132 std::string class_type{"class"};
133 if (c.is_abstract())
134 class_type = "abstract";
135
136 ostr << class_type << " " << c.alias();
137
138 if (c.is_union())
139 ostr << " "
140 << "<<union>>";
141
142 if (config().generate_links) {
144 }
145
146 generate_style(ostr, c.type_name(), c);
147
148 ostr << " {" << '\n';
149
150 //
151 // Process methods
152 //
153 if (config().group_methods()) {
155 }
156 else {
157 generate_methods(c.methods(), ostr);
158 }
159
160 //
161 // Process relationships - here only generate the set of
162 // rendered_relationships we'll generate them in a seperate method
163 //
164 std::set<std::string> rendered_relations;
165
166 std::stringstream all_relations_str;
167 for (const auto &r : c.relationships()) {
168 try {
169 generate_relationship(r, rendered_relations);
170 }
171 catch (error::uml_alias_missing &e) {
172 LOG_DBG("Skipping {} relation from {} to {} due "
173 "to: {}",
174 to_string(r.type()), c.full_name(), r.destination(), e.what());
175 }
176 }
177
178 //
179 // Process members
180 //
181 std::vector<clanguml::class_diagram::model::class_member> members{
182 c.members()};
183
184 sort_class_elements(members);
185
186 if (config().group_methods())
187 ostr << "__\n";
188
189 for (const auto &m : members) {
190 if (!config().include_relations_also_as_members() &&
191 rendered_relations.find(m.name()) != rendered_relations.end())
192 continue;
193
194 generate_member(m, ostr);
195
196 ostr << '\n';
197 }
198
199 ostr << "}" << '\n';
200
201 generate_notes(ostr, c);
202
203 for (const auto &member : c.members())
204 generate_member_notes(ostr, member, c.alias());
205
206 for (const auto &method : c.methods())
207 generate_member_notes(ostr, method, c.alias());
208}
209
211 const method_groups_t &methods, std::ostream &ostr) const
212{
213 bool is_first_non_empty_group{true};
214
215 for (const auto &group : method_groups_) {
216 const auto &group_methods = methods.at(group);
217 if (!group_methods.empty()) {
218 if (!is_first_non_empty_group)
219 ostr << "..\n";
220 is_first_non_empty_group = false;
222 }
223 }
224}
225
227 const std::vector<class_method> &methods, std::ostream &ostr) const
228{
229 auto sorted_methods = methods;
230 sort_class_elements(sorted_methods);
231
232 for (const auto &m : sorted_methods) {
233 generate_method(m, ostr);
234 ostr << '\n';
235 }
236}
237
239 const std::vector<class_method> &methods) const
240{
241 std::map<std::string, std::vector<class_method>> result;
242
243 for (const auto &g : method_groups_) {
244 result[g] = {};
245 }
246
247 for (const auto &m : methods) {
248 if (m.is_constructor() || m.is_destructor()) {
249 result["constructors"].push_back(m);
250 }
251 else if (m.is_copy_assignment() || m.is_move_assignment()) {
252 result["assignment"].push_back(m);
253 }
254 else if (m.is_operator()) {
255 result["operators"].push_back(m);
256 }
257 else {
258 result["other"].push_back(m);
259 }
260 }
261
262 return result;
263}
264
266 const class_diagram::model::class_method &m, std::ostream &ostr) const
267{
268 namespace plantuml_common = clanguml::common::generators::plantuml;
269 const auto &uns = config().using_namespace();
270
271 constexpr auto kAbbreviatedMethodArgumentsLength{15};
272
273 print_debug(m, ostr);
274
275 if (m.is_pure_virtual())
276 ostr << "{abstract} ";
277
278 if (m.is_static())
279 ostr << "{static} ";
280
281 std::string type{uns.relative(config().simplify_template_type(m.type()))};
282
283 ostr << plantuml_common::to_plantuml(m.access()) << m.name();
284
285 if (!m.template_params().empty()) {
286 m.render_template_params(ostr, config().using_namespace(), false);
287 }
288
289 ostr << "(";
290 if (config().generate_method_arguments() !=
292 std::vector<std::string> params;
293 std::transform(m.parameters().cbegin(), m.parameters().cend(),
294 std::back_inserter(params), [this](const auto &mp) {
295 return config().simplify_template_type(
296 mp.to_string(config().using_namespace()));
297 });
298 auto args_string = fmt::format("{}", fmt::join(params, ", "));
299 if (config().generate_method_arguments() ==
301 args_string = clanguml::util::abbreviate(
302 args_string, kAbbreviatedMethodArgumentsLength);
303 }
304 ostr << args_string;
305 }
306 ostr << ")";
307
308 if (m.is_constexpr())
309 ostr << " constexpr";
310 else if (m.is_consteval())
311 ostr << " consteval";
312
313 if (m.is_const())
314 ostr << " const";
315
316 if (m.is_noexcept())
317 ostr << " noexcept";
318
319 assert(!(m.is_pure_virtual() && m.is_defaulted()));
320
321 if (m.is_pure_virtual())
322 ostr << " = 0";
323
324 if (m.is_defaulted())
325 ostr << " = default";
326 else if (m.is_deleted())
327 ostr << " = deleted";
328
329 if (m.is_coroutine())
330 ostr << " [coroutine]";
331
332 ostr << " : " << type;
333
334 if (config().generate_links) {
335 generate_link(ostr, m);
336 }
337}
338
340 const class_diagram::model::class_member &m, std::ostream &ostr) const
341{
342 namespace plantuml_common = clanguml::common::generators::plantuml;
343 const auto &uns = config().using_namespace();
344
345 print_debug(m, ostr);
346
347 if (m.is_static())
348 ostr << "{static} ";
349
350 ostr << plantuml_common::to_plantuml(m.access()) << m.name() << " : "
351 << render_name(
352 uns.relative(config().simplify_template_type(m.type())));
353
354 if (config().generate_links) {
355 generate_link(ostr, m);
356 }
357}
358
359void generator::generate(const concept_ &c, std::ostream &ostr) const
360{
361 std::string class_type{"class"};
362
363 ostr << class_type << " " << c.alias() << " <<concept>>";
364
365 if (config().generate_links) {
367 }
368
369 generate_style(ostr, c.type_name(), c);
370
371 ostr << " {" << '\n';
372
373 if (config().generate_concept_requirements() &&
374 (c.requires_parameters().size() + c.requires_statements().size() > 0)) {
375 std::vector<std::string> parameters;
376 parameters.reserve(c.requires_parameters().size());
377 for (const auto &p : c.requires_parameters()) {
378 parameters.emplace_back(p.to_string(config().using_namespace()));
379 }
380
381 ostr << fmt::format("({})\n", fmt::join(parameters, ","));
382
383 ostr << "..\n";
384
385 ostr << fmt::format("{}\n", fmt::join(c.requires_statements(), "\n"));
386 }
387
388 ostr << "}" << '\n';
389}
390
391void generator::generate_member_notes(std::ostream &ostr,
392 const class_element &member, const std::string &alias) const
393{
394 for (const auto &decorator : member.decorators()) {
395 auto note = std::dynamic_pointer_cast<decorators::note>(decorator);
396 if (note && note->applies_to_diagram(config().name)) {
397 ostr << "note " << note->position << " of " << alias
398 << "::" << member.name() << '\n'
399 << note->text << '\n'
400 << "end note\n";
401 }
402 }
403}
404
405void generator::generate_relationships(std::ostream &ostr) const
406{
407 for (const auto &p : model()) {
408 if (auto *pkg = dynamic_cast<package *>(p.get()); pkg) {
409 generate_relationships(*pkg, ostr);
410 }
411 else if (auto *cls = dynamic_cast<class_ *>(p.get()); cls) {
412 generate_relationships(*cls, ostr);
413 }
414 else if (auto *enm = dynamic_cast<enum_ *>(p.get()); enm) {
415 generate_relationships(*enm, ostr);
416 }
417 else if (auto *cpt = dynamic_cast<concept_ *>(p.get()); cpt) {
418 generate_relationships(*cpt, ostr);
419 }
420 }
421}
422
424 const relationship &r, std::set<std::string> &rendered_relations) const
425{
426 namespace plantuml_common = clanguml::common::generators::plantuml;
427
428 LOG_DBG("Processing relationship {}", to_string(r.type()));
429
430 std::string destination;
431
432 auto target_element = model().get(r.destination());
433 if (!target_element.has_value())
434 throw error::uml_alias_missing{fmt::format(
435 "Missing element in the model for ID: {}", r.destination())};
436
437 destination = target_element.value().full_name(false);
438
439 if (util::starts_with(destination, std::string{"::"}))
440 destination = destination.substr(2, destination.size());
441
442 std::string puml_relation;
443 if (!r.multiplicity_source().empty())
444 puml_relation += "\"" + r.multiplicity_source() + "\" ";
445
446 puml_relation += plantuml_common::to_plantuml(r, config());
447
448 if (!r.multiplicity_destination().empty())
449 puml_relation += " \"" + r.multiplicity_destination() + "\"";
450
451 if (!r.label().empty()) {
452 rendered_relations.emplace(r.label());
453 }
454}
455
457 const class_ &c, std::ostream &ostr) const
458{
459 namespace plantuml_common = clanguml::common::generators::plantuml;
460
461 //
462 // Process relationships
463 //
464 std::set<std::string> rendered_relations;
465
466 std::stringstream all_relations_str;
467 std::set<std::string> unique_relations;
468
469 for (const auto &r : c.relationships()) {
470 LOG_DBG("== Processing relationship {}",
471 plantuml_common::to_plantuml(r, config()));
472
473 std::stringstream relstr;
474 eid_t destination{};
475 try {
476 destination = r.destination();
477
478 std::string puml_relation;
479 if (!r.multiplicity_source().empty())
480 puml_relation += "\"" + r.multiplicity_source() + "\" ";
481
482 puml_relation += plantuml_common::to_plantuml(r, config());
483
484 if (!r.multiplicity_destination().empty())
485 puml_relation += " \"" + r.multiplicity_destination() + "\"";
486
487 std::string target_alias;
488 try {
489 target_alias = model().to_alias(destination);
490 }
491 catch (...) {
492 LOG_DBG("Failed to find alias to {}", destination);
493 continue;
494 }
495
496 if (m_generated_aliases.find(target_alias) ==
498 continue;
499
500 relstr << c.alias() << " " << puml_relation << " " << target_alias;
501
502 if (!r.label().empty()) {
503 relstr << " : " << plantuml_common::to_plantuml(r.access())
504 << r.label();
505 rendered_relations.emplace(r.label());
506 }
507
508 if (unique_relations.count(relstr.str()) == 0) {
509 unique_relations.emplace(relstr.str());
510
511 relstr << '\n';
512
513 LOG_DBG("=== Adding relation {}", relstr.str());
514
515 all_relations_str << relstr.str();
516 }
517 }
518 catch (error::uml_alias_missing &e) {
519 LOG_DBG("=== Skipping {} relation from {} to {} due "
520 "to: {}",
521 to_string(r.type()), c.full_name(), destination, e.what());
522 }
523 }
524
525 if (model().should_include(relationship_t::kExtension)) {
526 for (const auto &b : c.parents()) {
527 std::stringstream relstr;
528 try {
529 auto target_alias = model().to_alias(b.id());
530
531 if (m_generated_aliases.find(target_alias) ==
533 continue;
534
535 relstr << target_alias << " <|-- " << c.alias() << '\n';
536 all_relations_str << relstr.str();
537 }
538 catch (error::uml_alias_missing &e) {
539 LOG_DBG("=== Skipping inheritance relation from {} to {} due "
540 "to: {}",
541 b.name(), c.name(), e.what());
542 }
543 }
544 }
545
546 ostr << all_relations_str.str();
547}
548
550 const concept_ &c, std::ostream &ostr) const
551{
552 namespace plantuml_common = clanguml::common::generators::plantuml;
553
554 //
555 // Process relationships
556 //
557 std::set<std::string> rendered_relations;
558
559 std::stringstream all_relations_str;
560 std::set<std::string> unique_relations;
561
562 for (const auto &r : c.relationships()) {
563 if (!model().should_include(r.type()))
564 continue;
565
566 LOG_DBG("== Processing relationship {}", to_string(r.type()));
567
568 std::stringstream relstr;
569 eid_t destination{};
570 try {
571 destination = r.destination();
572
573 std::string puml_relation;
574 if (!r.multiplicity_source().empty())
575 puml_relation += "\"" + r.multiplicity_source() + "\" ";
576
577 puml_relation += plantuml_common::to_plantuml(r, config());
578
579 if (!r.multiplicity_destination().empty())
580 puml_relation += " \"" + r.multiplicity_destination() + "\"";
581
582 std::string target_alias;
583 try {
584 target_alias = model().to_alias(destination);
585 }
586 catch (...) {
587 LOG_DBG("Failed to find alias to {}", destination);
588 continue;
589 }
590
591 if (m_generated_aliases.find(target_alias) ==
593 continue;
594
595 relstr << c.alias() << " " << puml_relation << " " << target_alias;
596
597 if (!r.label().empty()) {
598 relstr << " : " << plantuml_common::to_plantuml(r.access())
599 << r.label();
600 rendered_relations.emplace(r.label());
601 }
602
603 if (unique_relations.count(relstr.str()) == 0) {
604 unique_relations.emplace(relstr.str());
605
606 relstr << '\n';
607
608 LOG_DBG("=== Adding relation {}", relstr.str());
609
610 all_relations_str << relstr.str();
611 }
612 }
613 catch (error::uml_alias_missing &e) {
614 LOG_DBG("=== Skipping {} relation from {} to {} due "
615 "to: {}",
616 to_string(r.type()), c.full_name(), destination, e.what());
617 }
618 }
619
620 ostr << all_relations_str.str();
621}
622
623void generator::generate(const enum_ &e, std::ostream &ostr) const
624{
625 ostr << "enum " << e.alias();
626
627 if (config().generate_links) {
629 }
630
631 generate_style(ostr, e.type_name(), e);
632
633 ostr << " {" << '\n';
634
635 for (const auto &enum_constant : e.constants()) {
636 ostr << enum_constant << '\n';
637 }
638
639 ostr << "}" << '\n';
640
641 generate_notes(ostr, e);
642}
643
644void generator::generate_relationships(const enum_ &e, std::ostream &ostr) const
645{
646 for (const auto &r : e.relationships()) {
647 eid_t destination{};
648 std::stringstream relstr;
649 try {
650 destination = r.destination();
651
652 auto target_alias = model().to_alias(destination);
653
654 if (m_generated_aliases.find(target_alias) ==
656 continue;
657
658 relstr << e.alias() << " "
660 r, config())
661 << " " << target_alias;
662
663 if (!r.label().empty())
664 relstr << " : " << r.label();
665
666 relstr << '\n';
667
668 ostr << relstr.str();
669 }
670 catch (error::uml_alias_missing &ex) {
671 LOG_DBG("Skipping {} relation from {} to {} due "
672 "to: {}",
674 r, config()),
675 e.full_name(), destination, ex.what());
676 }
677 }
678}
679
680void generator::generate(const package &p, std::ostream &ostr) const
681{
682 const auto &uns = config().using_namespace();
683
684 if (config().generate_packages()) {
685 LOG_DBG("Generating package {}", p.name());
686
687 // Don't generate packages from namespaces filtered out by
688 // using_namespace
689 if (!uns.starts_with({p.full_name(false)})) {
690 print_debug(p, ostr);
691 ostr << "package [" << p.name() << "] ";
692 ostr << "as " << p.alias();
693
694 if (p.is_deprecated())
695 ostr << " <<deprecated>>";
696
697 generate_style(ostr, p.type_name(), p);
698
699 ostr << " {" << '\n';
700 }
701 }
702
703 for (const auto &subpackage : p) {
704 if (dynamic_cast<package *>(subpackage.get()) != nullptr) {
705 // TODO: add option - generate_empty_packages
706 const auto &sp = dynamic_cast<package &>(*subpackage);
707 if (!sp.is_empty()) {
708 together_group_stack_.enter();
709
710 generate(sp, ostr);
711
712 together_group_stack_.leave();
713 }
714 }
715 else if (auto *cls = dynamic_cast<class_ *>(subpackage.get()); cls) {
716 if (model().should_include(*subpackage)) {
717 auto together_group =
718 config().get_together_group(cls->full_name(false));
719 if (together_group) {
720 together_group_stack_.group_together(
721 together_group.value(), cls);
722 }
723 else {
724 generate_alias(*cls, ostr);
725 generate(*cls, ostr);
726 }
727 }
728 }
729 else if (auto *enm = dynamic_cast<enum_ *>(subpackage.get()); enm) {
730 if (model().should_include(*subpackage)) {
731 auto together_group =
732 config().get_together_group(subpackage->full_name(false));
733 if (together_group) {
734 together_group_stack_.group_together(
735 together_group.value(), enm);
736 }
737 else {
738 generate_alias(*enm, ostr);
739 generate(*enm, ostr);
740 }
741 }
742 }
743 else if (auto *cpt = dynamic_cast<concept_ *>(subpackage.get()); cpt) {
744 if (model().should_include(*subpackage)) {
745 auto together_group =
746 config().get_together_group(cpt->full_name(false));
747 if (together_group) {
748 together_group_stack_.group_together(
749 together_group.value(), cpt);
750 }
751 else {
752 generate_alias(*cpt, ostr);
753 generate(*cpt, ostr);
754 }
755 }
756 }
757 }
758
759 if (config().generate_packages()) {
760 // Now generate any diagram elements which are in together
761 // groups
762 for (const auto &[group_name, group_elements] :
763 together_group_stack_.get_current_groups()) {
764 ostr << "together {\n";
765
766 for (auto *e : group_elements) {
767 if (auto *cls = dynamic_cast<class_ *>(e); cls) {
768 generate_alias(*cls, ostr);
769 generate(*cls, ostr);
770 }
771 if (auto *enm = dynamic_cast<enum_ *>(e); enm) {
772 generate_alias(*enm, ostr);
773 generate(*enm, ostr);
774 }
775 if (auto *cpt = dynamic_cast<concept_ *>(e); cpt) {
776 generate_alias(*cpt, ostr);
777 generate(*cpt, ostr);
778 }
779 }
780
781 ostr << "}\n";
782 }
783
784 // Don't generate packages from namespaces filtered out by
785 // using_namespace
786 if (!uns.starts_with({p.full_name(false)})) {
787 ostr << "}" << '\n';
788 generate_notes(ostr, p);
789 }
790 }
791}
792
794 const package &p, std::ostream &ostr) const
795{
796 for (const auto &subpackage : p) {
797 if (dynamic_cast<package *>(subpackage.get()) != nullptr) {
798 // TODO: add option - generate_empty_packages, currently
799 // packages which do not contain anything but other
800 // packages are skipped
801 const auto &sp = dynamic_cast<package &>(*subpackage);
802 if (!sp.is_empty())
803 generate_relationships(sp, ostr);
804 }
805 else if (dynamic_cast<class_ *>(subpackage.get()) != nullptr) {
806 if (model().should_include(*subpackage)) {
808 dynamic_cast<class_ &>(*subpackage), ostr);
809 }
810 }
811 else if (dynamic_cast<enum_ *>(subpackage.get()) != nullptr) {
812 if (model().should_include(*subpackage)) {
814 dynamic_cast<enum_ &>(*subpackage), ostr);
815 }
816 }
817 else if (dynamic_cast<concept_ *>(subpackage.get()) != nullptr) {
818 if (model().should_include(*subpackage)) {
820 dynamic_cast<concept_ &>(*subpackage), ostr);
821 }
822 }
823 }
824}
825
826void generator::generate_diagram(std::ostream &ostr) const
827{
829
830 generate_groups(ostr);
831
833
835}
836
837void generator::generate_top_level_elements(std::ostream &ostr) const
838{
839 for (const auto &p : model()) {
840 if (auto *pkg = dynamic_cast<package *>(p.get()); pkg) {
841 if (!pkg->is_empty())
842 generate(*pkg, ostr);
843 }
844 else if (auto *cls = dynamic_cast<class_ *>(p.get()); cls) {
845 auto together_group =
846 config().get_together_group(cls->full_name(false));
847 if (together_group) {
848 together_group_stack_.group_together(
849 together_group.value(), cls);
850 }
851 else {
852 generate_alias(*cls, ostr);
853 generate(*cls, ostr);
854 }
855 }
856 else if (auto *enm = dynamic_cast<enum_ *>(p.get()); enm) {
857 auto together_group =
858 config().get_together_group(enm->full_name(false));
859 if (together_group) {
860 together_group_stack_.group_together(
861 together_group.value(), enm);
862 }
863 else {
864 generate_alias(*enm, ostr);
865 generate(*enm, ostr);
866 }
867 }
868 else if (auto *cpt = dynamic_cast<concept_ *>(p.get()); cpt) {
869 auto together_group =
870 config().get_together_group(cpt->full_name(false));
871 if (together_group) {
872 together_group_stack_.group_together(
873 together_group.value(), cpt);
874 }
875 else {
876 generate_alias(*cpt, ostr);
877 generate(*cpt, ostr);
878 }
879 }
880 }
881}
882
883void generator::generate_groups(std::ostream &ostr) const
884{
885 for (const auto &[group_name, group_elements] :
886 together_group_stack_.get_current_groups()) {
887 ostr << "together {\n";
888
889 for (auto *e : group_elements) {
890 if (auto *cls = dynamic_cast<class_ *>(e); cls) {
891 generate_alias(*cls, ostr);
892 generate(*cls, ostr);
893 }
894 if (auto *enm = dynamic_cast<enum_ *>(e); enm) {
895 generate_alias(*enm, ostr);
896 generate(*enm, ostr);
897 }
898 if (auto *cpt = dynamic_cast<concept_ *>(e); cpt) {
899 generate_alias(*cpt, ostr);
900 generate(*cpt, ostr);
901 }
902 }
903
904 ostr << "}\n";
905 }
906}
907
908} // namespace clanguml::class_diagram::generators::plantuml