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