0.6.0
C++ to UML diagram generator based on Clang
Loading...
Searching...
No Matches
diagram.cc
Go to the documentation of this file.
1/**
2 * @file src/sequence_diagram/model/diagram.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
19#include "diagram.h"
20
22
23#include <functional>
24#include <memory>
25
27
28std::vector<std::vector<eid_t>> find_reverse_message_chains(
30{
31 std::vector<std::vector<eid_t>> all_message_chains;
32 std::vector<eid_t> current_chain;
33
34 // Depth first search lambda function to traverse reverse call graph
35 std::function<void(const reverse_call_graph_activity_node &)> dfs =
36 [&](const reverse_call_graph_activity_node &node) {
37 // Add the current node’s activity ID to the path
38 current_chain.push_back(node.activity_id);
39
40 if (node.callers.empty()) {
41 auto reversed = current_chain;
42 std::reverse(reversed.begin(), reversed.end());
43 all_message_chains.emplace_back(std::move(reversed));
44 }
45 else {
46 for (const auto &child : node.callers) {
47 dfs(child);
48 }
49 }
50
51 // Backtrack
52 current_chain.pop_back();
53 };
54
55 // Start depth first search
56 dfs(root);
57
58 return all_message_chains;
59}
60
62{
64}
65
67 const std::string &full_name) const
68{
69 for (const auto &[id, participant] : participants_) {
70 if (participant->full_name(false) == full_name)
71 return {*participant};
72 }
73
74 return {};
75}
76
78 const eid_t id) const
79{
80 if (participants_.find(id) != participants_.end())
81 return {*participants_.at(id)};
82
83 return {};
84}
85
86std::string diagram::to_alias(const std::string &full_name) const
87{
88 return full_name;
89}
90
91void diagram::add_participant(std::unique_ptr<participant> p)
92{
93 const auto participant_id = p->id();
94
95 p->complete(true);
96
97 assert(participant_id.is_global());
98
99 if (participants_.find(participant_id) == participants_.end()) {
100 LOG_DBG("Adding '{}' participant: {}, {} [{}]", p->type_name(),
101 p->full_name(false), p->id(),
102 p->type_name() == "method"
103 ? dynamic_cast<method *>(p.get())->method_name()
104 : "");
105
106 participants_.emplace(participant_id, std::move(p));
107 }
108}
109
111{
112 active_participants_.emplace(id);
113}
114
116{
117 return activities_.at(id);
118}
119
120bool diagram::has_activity(eid_t id) const { return activities_.count(id) > 0; }
121
123
125{
126 const auto caller_id = message.from();
127 const auto callee_id = message.to();
128
129 if (activities_.find(caller_id) == activities_.end()) {
130 activity a{caller_id};
131 activities_.insert({caller_id, std::move(a)});
132 }
133
134 if (activities_.find(callee_id) == activities_.end()) {
135 activity a{callee_id};
136 activities_.insert({callee_id, std::move(a)});
137 }
138
139 get_activity(callee_id).add_caller(caller_id);
140 get_activity(caller_id).add_message(std::move(message));
141}
142
144{
145 add_message(std::move(message));
146}
147
150{
151 const auto caller_id = message.from();
152
153 if (activities_.find(caller_id) != activities_.end()) {
154 auto &current_messages = get_activity(caller_id).messages();
155
157 std::move(message), start_type, current_messages);
158 }
159}
160
162{
164 const auto caller_id = m.from();
165
166 if (activities_.find(caller_id) != activities_.end()) {
167 auto &current_messages = get_activity(caller_id).messages();
168
169 if (current_messages.back().type() == message_t::kCase) {
170 // Do nothing - fallthroughs not supported yet...
171 }
172 else {
173 current_messages.emplace_back(std::move(m));
174 }
175 }
176}
177
178std::map<eid_t, activity> &diagram::sequences() { return activities_; }
179
180const std::map<eid_t, activity> &diagram::sequences() const
181{
182 return activities_;
183}
184
185std::map<eid_t, std::unique_ptr<participant>> &diagram::participants()
186{
187 return participants_;
188}
189
190const std::map<eid_t, std::unique_ptr<participant>> &
192{
193 return participants_;
194}
195
197
198const std::set<eid_t> &diagram::active_participants() const
199{
201}
202
205{
206 return filter().should_include(p) &&
208 dynamic_cast<const common::model::source_location &>(p));
209}
210
211std::vector<std::string> diagram::list_from_values() const
212{
213 std::vector<std::string> result;
214
215 for (const auto &[from_id, act] : activities_) {
216
217 const auto &from_activity = *(participants_.at(from_id));
218 const auto &full_name = from_activity.full_name(false);
219 if (!full_name.empty())
220 result.push_back(full_name);
221 }
222
223 std::sort(result.begin(), result.end());
224 result.erase(std::unique(result.begin(), result.end()), result.end());
225
226 return result;
227}
228
229std::vector<std::string> diagram::list_to_values() const
230{
231 std::vector<std::string> result;
232
233 for (const auto &[from_id, act] : activities_) {
234 for (const auto &m : act.messages()) {
235 if (participants_.count(m.to()) > 0) {
236 const auto &to_activity = *(participants_.at(m.to()));
237 const auto &full_name = to_activity.full_name(false);
238 if (!full_name.empty())
239 result.push_back(full_name);
240 }
241 }
242 }
243
244 std::sort(result.begin(), result.end());
245 result.erase(std::unique(result.begin(), result.end()), result.end());
246
247 return result;
248}
249
251 const config::source_location &to_location) const
252{
253 std::vector<eid_t> to_activities{};
254
255 for (const auto &[k, v] : sequences()) {
256 for (const auto &m : v.messages()) {
257 if (m.type() != common::model::message_t::kCall)
258 continue;
259
260 const auto &callee = *participants().at(m.to());
261 std::string vto = callee.full_name(false);
262 if (to_location.location == vto) {
263 LOG_DBG(
264 "Found sequence diagram end point '{}': {}", vto, m.to());
265 to_activities.push_back(m.to());
266 }
267 }
268 }
269
270 if (to_activities.empty()) {
271 LOG_WARN("Failed to find 'to' participant {} for to "
272 "condition: ",
273 to_location.location.to_string());
274 }
275
276 util::remove_duplicates(to_activities);
277
278 return to_activities;
279}
280
282 const config::source_location &from_location) const
283{
284 std::vector<eid_t> from_activities{};
285
286 for (const auto &[k, v] : sequences()) {
287 if (participants().count(v.from()) == 0)
288 continue;
289
290 const auto &caller = *participants().at(v.from());
291 std::string vfrom = caller.full_name(false);
292 if (from_location.location == vfrom) {
293 LOG_DBG("Found sequence diagram start point '{}': {}", vfrom, k);
294 from_activities.push_back(k);
295 }
296 }
297
298 if (from_activities.empty()) {
299 LOG_WARN("Failed to find 'from' participant {} for from "
300 "condition: ",
301 from_location.location.to_string());
302 }
303
304 util::remove_duplicates(from_activities);
305
306 return from_activities;
307}
308
310 std::set<eid_t> visited_callers) const
311{
312 LOG_INFO("Building reverse call graph for activity: {}", node.activity_id);
313
314 if (sequences().count(node.activity_id) == 0)
315 return;
316
317 visited_callers.insert(node.activity_id);
318
319 const auto &callers = sequences().at(node.activity_id).callers();
320
321 for (const auto &caller : callers) {
322 if (visited_callers.count(caller) > 0) {
323 // break recursive calls
324 continue;
325 }
326
328 caller_node.activity_id = caller;
329
330 build_reverse_call_graph(caller_node, visited_callers);
331
332 node.callers.emplace_back(std::move(caller_node));
333 }
334}
335
336std::vector<message_chain_t> diagram::get_all_from_to_message_chains(
337 const eid_t from_activity, const eid_t to_activity) const
338{
339 // Message (call) chains matching the specified from_to condition
340 std::vector<message_chain_t> message_chains;
341
342 // First, build reverse call graph starting from target `to_activity`
343 // `target_roots` should contain all activities which call `to_activity`
345 target_roots.activity_id = to_activity;
346
347 for (const auto &[k, v] : sequences()) {
348 for (const auto &m : v.messages()) {
349 if (m.type() != common::model::message_t::kCall)
350 continue;
351
352 if (m.to() == to_activity) {
354 node.activity_id = m.from();
355 target_roots.callers.emplace_back(std::move(node));
356 }
357 }
358 }
359
360 // Now recurse from the initial target activities based on reverse
361 // callers list stored in each activity
362 for (auto &caller : target_roots.callers)
364
365 // Convert the reverse call graph into a list of call chains using
366 // depth first search
367 auto activity_id_chains = find_reverse_message_chains(target_roots);
368
369 // Make sure the activity call chains list is unique
370 sort(begin(activity_id_chains), end(activity_id_chains));
371 activity_id_chains.erase(
372 unique(begin(activity_id_chains), end(activity_id_chains)),
373 end(activity_id_chains));
374
375 // Convert the call chains with activity ids to lists of actual messages
376 for (const auto &chain : activity_id_chains) {
377 message_chain_t message_chain;
378 for (auto it = begin(chain); it != end(chain); it++) {
379 const auto next_it = it + 1;
380 if (next_it == end(chain))
381 break;
382
383 auto from_id = *it;
384 if (activities_.count(from_id) == 0)
385 continue;
386
387 auto to_id = *(next_it);
388
389 const auto &act = activities_.at(from_id);
390
391 for (const auto &m : act.messages()) {
392 if (m.to() == to_id) {
393 message_chain.push_back(m);
394 break;
395 }
396 }
397 }
398 message_chains.emplace_back(std::move(message_chain));
399 }
400
401 // Perform final filtering of the message chains
402 std::vector<message_chain_t> message_chains_filtered{};
403
404 for (auto &mc : message_chains) {
405 // Skip empty chains
406 if (mc.empty())
407 continue;
408
409 // Make sure the chain has valid starting point
410 if (from_activity.value() == 0 ||
411 (mc.front().from() == from_activity)) {
412 message_chains_filtered.push_back(mc);
413 }
414 }
415
416 int message_chain_index{};
417 for (const auto &mc : message_chains_filtered) {
418 LOG_INFO("\t{}: {}", message_chain_index++,
419 fmt::join(util::map<std::string>(mc,
420 [](const model::message &m) -> std::string {
421 return m.message_name();
422 }),
423 "->"));
424 }
425
426 return message_chains_filtered;
427}
428
430{
431 return activities_.empty() || participants_.empty();
432}
433
435{
436 std::map<eid_t, activity> activities;
437 std::map<eid_t, std::unique_ptr<participant>> participants;
438 std::set<eid_t> active_participants;
439
440 for (auto &[id, act] : sequences()) {
441 model::activity new_activity{id};
442
443 // If activity is a lambda operator() - skip it
444 auto maybe_lambda_activity = get_participant<model::method>(id);
445
446 if (maybe_lambda_activity) {
447 const auto parent_class_id =
448 maybe_lambda_activity.value().class_id();
449 auto maybe_parent_class =
450 get_participant<model::class_>(parent_class_id);
451
452 if (maybe_parent_class && maybe_parent_class.value().is_lambda()) {
453 continue;
454 }
455 }
456
457 // For other activities, check each message - if it calls lambda
458 // operator() - reattach the message to the next activity in the chain
459 // (assuming it's not lambda)
460 for (auto &m : act.messages()) {
461
462 auto message_call_to_lambda{false};
463
464 message_call_to_lambda =
465 inline_lambda_operator_call(id, new_activity, m);
466
467 if (!message_call_to_lambda)
468 new_activity.add_message(m);
469 }
470
471 // Add activity
472 activities.insert({id, std::move(new_activity)});
473 }
474
475 for (auto &&[id, p] : this->participants()) {
476 // Skip participants which are lambda classes
477 if (const auto *maybe_class =
478 dynamic_cast<const model::class_ *>(p.get());
479 maybe_class != nullptr && maybe_class->is_lambda()) {
480 continue;
481 }
482
483 // Skip participants which are lambda operator methods
484 if (const auto *maybe_method =
485 dynamic_cast<const model::method *>(p.get());
486 maybe_method != nullptr) {
487 auto maybe_class =
488 get_participant<model::class_>(maybe_method->class_id());
489 if (maybe_class && maybe_class.value().is_lambda())
490 continue;
491 }
492
493 // Otherwise move the participant to the new diagram model
494 auto participant_id = p->id();
495 participants.emplace(participant_id, std::move(p));
496 }
497
498 // Skip active participants which are not in lambdaless_diagram participants
499 for (auto id : this->active_participants()) {
500 if (participants.count(id) > 0) {
501 active_participants.emplace(id);
502 }
503 }
504
505 activities_ = std::move(activities);
506 participants_ = std::move(participants);
508}
509
511 const eid_t id, model::activity &new_activity, const model::message &m)
512{
513 bool message_call_to_lambda{false};
514 auto maybe_lambda_operator = get_participant<model::method>(m.to());
515
516 if (maybe_lambda_operator) {
517 const auto parent_class_id = maybe_lambda_operator.value().class_id();
518 auto maybe_parent_class =
519 get_participant<model::class_>(parent_class_id);
520
521 if (maybe_parent_class && maybe_parent_class.value().is_lambda()) {
522 if (has_activity(m.to())) {
523 auto lambda_operator_activity = get_activity(m.to());
524
525 // For each call in that lambda activity - reattach this
526 // call to the current activity
527 for (auto &mm : lambda_operator_activity.messages()) {
528 if (!inline_lambda_operator_call(id, new_activity, mm)) {
529 auto new_message{mm};
530
531 new_message.set_from(id);
532 new_activity.add_message(new_message);
533 }
534 }
535 }
536
537 message_call_to_lambda = true;
538 }
539 }
540
541 return message_call_to_lambda;
542}
543
544void diagram::print() const
545{
546 LOG_TRACE(" --- Participants ---");
547 for (const auto &[id, participant] : participants_) {
548 LOG_DBG("{} - {}", id, participant->to_string());
549 }
550
551 LOG_TRACE(" --- Activities ---");
552 for (const auto &[from_id, act] : activities_) {
553 if (participants_.count(from_id) == 0)
554 continue;
555
556 LOG_TRACE("Sequence id={}:", from_id);
557
558 const auto &from_activity = *(participants_.at(from_id));
559
560 LOG_TRACE(" Activity id={}, from={}:", act.from(),
561 from_activity.full_name(false));
562
563 for (const auto &message : act.messages()) {
564 if (participants_.find(message.from()) == participants_.end())
565 continue;
566
567 const auto &from_participant = *participants_.at(message.from());
568
569 if (participants_.find(message.to()) == participants_.end()) {
570 LOG_TRACE(" Message from={}, from_id={}, "
571 "to={}, to_id={}, name={}, type={}",
572 from_participant.full_name(false), from_participant.id(),
573 "__UNRESOLVABLE_ID__", message.to(), message.message_name(),
574 to_string(message.type()));
575 }
576 else {
577 const auto &to_participant = *participants_.at(message.to());
578
579 std::string message_comment{"None"};
580 if (const auto &cmt = message.comment(); cmt.has_value()) {
581 message_comment = cmt.value().at("comment");
582 }
583
584 LOG_TRACE(" Message from={}, from_id={}, "
585 "to={}, to_id={}, name={}, type={}, comment={}",
586 from_participant.full_name(false), from_participant.id(),
587 to_participant.full_name(false), to_participant.id(),
588 message.message_name(), to_string(message.type()),
589 message_comment);
590 }
591 }
592 }
593}
594
596 const common::model::message_t statement_begin,
597 std::vector<message> &current_messages) const
598{
599 bool is_empty_statement{true};
600
601 auto rit = current_messages.rbegin();
602 for (; rit != current_messages.rend(); rit++) {
603 if (rit->type() == statement_begin) {
604 break;
605 }
606 if (rit->type() == common::model::message_t::kCall) {
607 is_empty_statement = false;
608 break;
609 }
610 }
611
612 if (is_empty_statement) {
613 current_messages.erase((rit + 1).base(), current_messages.end());
614 }
615 else {
616 current_messages.emplace_back(std::move(m));
617 }
618}
619
621{
622 // Apply diagram filters and remove any empty block statements
624
625 // First in each sequence (activity) filter out any remaining
626 // uninteresting calls
627 for (auto &[id, act] : activities_) {
628 util::erase_if(act.messages(), [this](auto &m) {
629 if (m.type() != message_t::kCall)
630 return false;
631
632 const auto &to = get_participant<model::participant>(m.to());
633 if (!to || to.value().skip())
634 return true;
635
636 if (!should_include(to.value())) {
637 LOG_DBG("Excluding call from [{}] to {} [{}]", m.from(),
638 to.value().full_name(false), m.to());
639 return true;
640 }
641
642 return false;
643 });
644 }
645
646 // Now remove any empty block statements, e.g. if/endif
647 for (auto &[id, act] : activities_) {
648 int64_t block_nest_level{0};
649 std::vector<std::vector<message>> block_message_stack;
650 // Add first stack level - this level will contain the filtered
651 // message sequence
652 block_message_stack.emplace_back();
653
654 // First create a recursive stack from the messages and
655 // message blocks (e.g. if statements)
656 for (auto &m : act.messages()) {
657 if (is_begin_block_message(m.type())) {
658 block_nest_level++;
659 block_message_stack.push_back({m});
660 }
661 else if (is_end_block_message(m.type())) {
662 block_nest_level--;
663
664 block_message_stack.back().push_back(m);
665
666 // Check the last stack for any calls, if yes, collapse it
667 // on the previous stack
668 if (std::count_if(block_message_stack.back().begin(),
669 block_message_stack.back().end(), [](auto &m) {
670 return m.type() == message_t::kCall;
671 }) > 0) {
672 std::copy(block_message_stack.back().begin(),
673 block_message_stack.back().end(),
674 std::back_inserter(
675 block_message_stack.at(block_nest_level)));
676 }
677
678 block_message_stack.pop_back();
679
680 assert(block_nest_level >= 0);
681 }
682 else {
683 if (m.type() == message_t::kCall) {
684 // Set the message return type based on the callee return
685 // type
686 auto to_participant =
687 get_participant<sequence_diagram::model::function>(
688 m.to());
689 if (to_participant.has_value()) {
690 m.set_return_type(to_participant.value().return_type());
691 }
692 }
693 block_message_stack.back().push_back(m);
694 }
695 }
696
697 act.messages().clear();
698
699 for (auto &m : block_message_stack[0]) {
700 act.add_message(m);
701 }
702 }
703}
704} // namespace clanguml::sequence_diagram::model
705
706namespace clanguml::common::model {
707template <>
708bool check_diagram_type<clanguml::sequence_diagram::model::diagram>(diagram_t t)
709{
710 return t == diagram_t::kSequence;
711}
712}