diff --git a/.clang-uml b/.clang-uml index 14bdc662d..b503c0c26 100644 --- a/.clang-uml +++ b/.clang-uml @@ -15,6 +15,7 @@ mermaid: generate_links: link: "{% if existsIn(element, \"doxygen_link\") %}{{ element.doxygen_link }}{% endif %}" tooltip: "{% if existsIn(element, \"comment\") and existsIn(element.comment, \"brief\") %}{{ abbrv(trim(replace(element.comment.brief.0, \"\\n+\", \" \")), 256) }}{% else %}{{ element.name }}{% endif %}" +fold_repeated_activities: true diagrams: # Class diagrams class_translation_unit_visitor: diff --git a/docs/sequence_diagrams.md b/docs/sequence_diagrams.md index d07574e07..fbf36e8ec 100644 --- a/docs/sequence_diagrams.md +++ b/docs/sequence_diagrams.md @@ -9,6 +9,7 @@ * [Customizing participants order](#customizing-participants-order) * [Generating return types](#generating-return-types) * [Generating condition statements](#generating-condition-statements) +* [Folding repeated activities](#folding-repeated-activities) * [Injecting call expressions manually through comments](#injecting-call-expressions-manually-through-comments) * [Including comments in sequence diagrams](#including-comments-in-sequence-diagrams) @@ -325,6 +326,23 @@ generate_condition_statements: true An example of a diagram with this feature enabled is presented below: ![extension](test_cases/t20033_sequence.svg) +## Folding repeated activities +If in a given sequence diagram functions or methods are called multiple times +from different branches, each of these activities will be rendered fully +which can mean that the diagram be very large. + +In order to minimize the diagram size in such situations, an option can be set: + +```yaml +fold_repeated_activities: true +``` + +which will render any activity only once, and any further calls to that activity +will only render a call to the activity and an indicator - a single `*` character +- in a note over the activity. + +For an example of this see the test case [t20056](test_cases/t20056.md). + ## Injecting call expressions manually through comments In some cases, `clang-uml` is not yet able to discover a call expression target in some line of code. This can include passing function or method address to diff --git a/src/cli/cli_handler.cc b/src/cli/cli_handler.cc index 9841cc9ef..808d47d68 100644 --- a/src/cli/cli_handler.cc +++ b/src/cli/cli_handler.cc @@ -569,6 +569,7 @@ cli_flow_t cli_handler::add_config_diagram( true; doc["diagrams"][name]["inline_lambda_messages"] = false; doc["diagrams"][name]["generate_message_comments"] = false; + doc["diagrams"][name]["fold_repeated_activities"] = false; doc["diagrams"][name]["generate_condition_statements"] = false; doc["diagrams"][name]["using_namespace"] = std::vector{{"myproject"}}; diff --git a/src/config/config.cc b/src/config/config.cc index aa073cec2..d20a96441 100644 --- a/src/config/config.cc +++ b/src/config/config.cc @@ -248,6 +248,7 @@ void inheritable_diagram_options::inherit( puml.override(parent.puml); mermaid.override(parent.mermaid); generate_method_arguments.override(parent.generate_method_arguments); + fold_repeated_activities.override(parent.fold_repeated_activities); generate_concept_requirements.override( parent.generate_concept_requirements); generate_packages.override(parent.generate_packages); diff --git a/src/config/config.h b/src/config/config.h index 67f3dd006..0e15ce19a 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -606,6 +606,7 @@ struct inheritable_diagram_options { "generate_condition_statements", false}; option> participants_order{"participants_order"}; option generate_message_comments{"generate_message_comments", false}; + option fold_repeated_activities{"fold_repeated_activities", false}; option message_comment_width{ "message_comment_width", clanguml::util::kDefaultMessageCommentWidth}; option debug_mode{"debug_mode", false}; diff --git a/src/config/schema.h b/src/config/schema.h index 1f8bb767f..995810c3e 100644 --- a/src/config/schema.h +++ b/src/config/schema.h @@ -249,6 +249,7 @@ const std::string schema_str = R"( generate_return_types: !optional bool generate_condition_statements: !optional bool generate_message_comments: !optional bool + fold_repeated_activities: !optional bool message_comment_width: !optional int participants_order: !optional [string] start_from: !optional [source_location_t] # deprecated -> 'from' @@ -377,6 +378,7 @@ const std::string schema_str = R"( generate_return_types: !optional bool generate_condition_statements: !optional bool generate_message_comments: !optional bool + fold_repeated_activities: !optional bool message_comment_width: !optional int generate_packages: !optional bool group_methods: !optional bool diff --git a/src/config/yaml_decoders.cc b/src/config/yaml_decoders.cc index 6235bd3b2..e86feb7d0 100644 --- a/src/config/yaml_decoders.cc +++ b/src/config/yaml_decoders.cc @@ -730,6 +730,7 @@ template <> struct convert { get_option(node, rhs.participants_order); get_option(node, rhs.generate_method_arguments); get_option(node, rhs.generate_message_comments); + get_option(node, rhs.fold_repeated_activities); get_option(node, rhs.message_comment_width); get_option(node, rhs.type_aliases); @@ -909,6 +910,7 @@ template <> struct convert { get_option(node, rhs.generate_return_types); get_option(node, rhs.generate_condition_statements); get_option(node, rhs.generate_message_comments); + get_option(node, rhs.fold_repeated_activities); get_option(node, rhs.message_comment_width); get_option(node, rhs.type_aliases); diff --git a/src/config/yaml_emitters.cc b/src/config/yaml_emitters.cc index bb6e77afa..0e0e66de7 100644 --- a/src/config/yaml_emitters.cc +++ b/src/config/yaml_emitters.cc @@ -353,6 +353,7 @@ YAML::Emitter &operator<<( out << c.generate_return_types; out << c.participants_order; out << c.generate_message_comments; + out << c.fold_repeated_activities; out << c.message_comment_width; } else if (const auto *pd = dynamic_cast(&c); diff --git a/src/sequence_diagram/generators/mermaid/sequence_diagram_generator.cc b/src/sequence_diagram/generators/mermaid/sequence_diagram_generator.cc index 4dd23ee65..e18fdc995 100644 --- a/src/sequence_diagram/generators/mermaid/sequence_diagram_generator.cc +++ b/src/sequence_diagram/generators/mermaid/sequence_diagram_generator.cc @@ -22,7 +22,6 @@ namespace clanguml::sequence_diagram::generators::mermaid { using clanguml::common::model::message_t; using clanguml::config::location_t; -using clanguml::sequence_diagram::model::activity; using clanguml::sequence_diagram::model::message; using namespace clanguml::util; @@ -195,8 +194,25 @@ void generator::generate_return(const message &m, std::ostream &ostr) const } void generator::generate_activity( - const activity &a, std::ostream &ostr, std::vector &visited) const + eid_t activity_id, std::ostream &ostr, std::vector &visited) const { + const auto &a = model().get_activity(activity_id); + + const auto [it, inserted] = generated_activities_.emplace(activity_id); + + if (config().fold_repeated_activities() && !inserted && + !a.messages().empty()) { + const auto &p = + model().get_participant(activity_id); + + if (p.has_value()) { + ostr << indent(1) << "Note over " << generate_alias(p.value()) + << " : *\n"; + } + + return; + } + for (const auto &m : a.messages()) { if (m.in_static_declaration_context()) { if (util::contains(already_generated_in_static_context_, m)) @@ -225,8 +241,7 @@ void generator::generate_activity( .end()) { // break infinite recursion on recursive calls LOG_DBG("Creating activity {} --> {} - missing sequence {}", m.from(), m.to(), m.to()); - generate_activity( - model().get_activity(m.to()), ostr, visited); + generate_activity(m.to(), ostr, visited); } } else @@ -642,8 +657,7 @@ void generator::generate_diagram(std::ostream &ostr) const ostr << indent(1) << "activate " << from_alias << '\n'; - generate_activity( - model().get_activity(start_from), ostr, visited_participants); + generate_activity(start_from, ostr, visited_participants); if (from.value().type_name() == "method" || config().combine_free_functions_into_file_participants()) { diff --git a/src/sequence_diagram/generators/mermaid/sequence_diagram_generator.h b/src/sequence_diagram/generators/mermaid/sequence_diagram_generator.h index e3fbf6c26..cea2f299f 100644 --- a/src/sequence_diagram/generators/mermaid/sequence_diagram_generator.h +++ b/src/sequence_diagram/generators/mermaid/sequence_diagram_generator.h @@ -113,13 +113,13 @@ class generator : public common_generator { /** * @brief Generate sequence diagram activity. * - * @param a Activity model + * @param activity_id Activity id * @param ostr Output stream * @param visited List of already visited participants, this is necessary * for breaking infinite recursion on recursive calls */ - void generate_activity(const clanguml::sequence_diagram::model::activity &a, - std::ostream &ostr, std::vector &visited) const; + void generate_activity(eid_t activity_id, std::ostream &ostr, + std::vector &visited) const; private: /** @@ -158,6 +158,7 @@ class generator : public common_generator { mutable std::set generated_participants_; mutable std::set generated_comment_ids_; mutable std::vector already_generated_in_static_context_; + mutable std::set generated_activities_; }; } // namespace mermaid diff --git a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc index 8f446f660..58ca54502 100644 --- a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc +++ b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc @@ -23,7 +23,6 @@ namespace clanguml::sequence_diagram::generators::plantuml { using clanguml::common::eid_t; using clanguml::common::model::message_t; using clanguml::config::location_t; -using clanguml::sequence_diagram::model::activity; using clanguml::sequence_diagram::model::message; using namespace clanguml::util; @@ -152,8 +151,27 @@ void generator::generate_return(const message &m, std::ostream &ostr) const } void generator::generate_activity( - const activity &a, std::ostream &ostr, std::vector &visited) const + eid_t activity_id, std::ostream &ostr, std::vector &visited) const { + const auto &a = model().get_activity(activity_id); + + const auto [it, inserted] = generated_activities_.emplace(activity_id); + + if (config().fold_repeated_activities() && !inserted && + !a.messages().empty()) { + const auto &p = + model().get_participant(activity_id); + + if (p.has_value()) { + ostr << "hnote over " << generate_alias(p.value()) << " : *\n"; + // This is necessary to keep the hnote over the activity life line + ostr << generate_alias(p.value()) << "-[hidden]->" + << generate_alias(p.value()) << '\n'; + } + + return; + } + for (const auto &m : a.messages()) { if (m.in_static_declaration_context()) { if (util::contains(already_generated_in_static_context_, m)) @@ -182,8 +200,7 @@ void generator::generate_activity( .end()) { // break infinite recursion on recursive calls LOG_DBG("Creating activity {} --> {} - missing sequence {}", m.from(), m.to(), m.to()); - generate_activity( - model().get_activity(m.to()), ostr, visited); + generate_activity(m.to(), ostr, visited); } } else @@ -651,8 +668,7 @@ void generator::generate_diagram(std::ostream &ostr) const ostr << "activate " << from_alias << '\n'; - generate_activity( - model().get_activity(start_from), ostr, visited_participants); + generate_activity(start_from, ostr, visited_participants); if (from.value().type_name() == "method" || config().combine_free_functions_into_file_participants()) { diff --git a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.h b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.h index c82262954..f4ec86d8e 100644 --- a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.h +++ b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.h @@ -107,13 +107,13 @@ class generator : public common_generator { /** * @brief Generate sequence diagram activity. * - * @param a Activity model + * @param activity_id Activity id * @param ostr Output stream * @param visited List of already visited participants, this is necessary * for breaking infinite recursion on recursive calls */ - void generate_activity(const clanguml::sequence_diagram::model::activity &a, - std::ostream &ostr, std::vector &visited) const; + void generate_activity(eid_t activity_id, std::ostream &ostr, + std::vector &visited) const; private: /** @@ -160,6 +160,7 @@ class generator : public common_generator { mutable std::set generated_participants_; mutable std::set generated_comment_ids_; mutable std::vector already_generated_in_static_context_; + mutable std::set generated_activities_; }; } // namespace plantuml diff --git a/tests/t20056/.clang-uml b/tests/t20056/.clang-uml new file mode 100644 index 000000000..97a6792b4 --- /dev/null +++ b/tests/t20056/.clang-uml @@ -0,0 +1,12 @@ +diagrams: + t20056_sequence: + type: sequence + glob: + - t20056.cc + include: + namespaces: + - clanguml::t20056 + using_namespace: clanguml::t20056 + fold_repeated_activities: true + from: + - function: "clanguml::t20056::tmain()" \ No newline at end of file diff --git a/tests/t20056/t20056.cc b/tests/t20056/t20056.cc new file mode 100644 index 000000000..3337fc8d3 --- /dev/null +++ b/tests/t20056/t20056.cc @@ -0,0 +1,46 @@ +namespace clanguml { +namespace t20056 { +struct A { + void a() { aa(); } + + void aa() { aaa(); } + + void aaa() { } +}; + +struct B { + void b() { bb(); } + + void bb() { bbb(); } + + void bbb() { a.a(); } + + A a; +}; + +struct C { + void c() { cc(); } + + void cc() { ccc(); } + + void ccc() { b.b(); } + + B b; +}; + +void tmain() +{ + A a; + B b; + C c; + + c.c(); + c.c(); + c.c(); + + b.b(); + + a.a(); +} +} +} diff --git a/tests/t20056/test_case.h b/tests/t20056/test_case.h new file mode 100644 index 000000000..c09852b76 --- /dev/null +++ b/tests/t20056/test_case.h @@ -0,0 +1,47 @@ +/** + * tests/t20056/test_case.h + * + * Copyright (c) 2021-2024 Bartek Kryza + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +TEST_CASE("t20056") +{ + using namespace clanguml::test; + using namespace std::string_literals; + + auto [config, db, diagram, model] = + CHECK_SEQUENCE_MODEL("t20056", "t20056_sequence"); + + CHECK_SEQUENCE_DIAGRAM(*config, diagram, *model, [](const auto &src) { + REQUIRE(MessageOrder(src, + { + // clang-format off + {"tmain()", "C", "c()"}, + {"C", "C", "cc()"}, + {"C", "C", "ccc()"}, + {"C", "B", "b()"}, + {"B", "B", "bb()"}, + {"B", "B", "bbb()"}, + {"B", "A", "a()"}, + {"A", "A", "aa()"}, + {"A", "A", "aaa()"}, + {"tmain()", "C", "c()"}, + {"tmain()", "C", "c()"}, + {"tmain()", "B", "b()"}, + {"tmain()", "A", "a()"}, + // clang-format on + })); + }); +} \ No newline at end of file diff --git a/tests/test_cases.cc b/tests/test_cases.cc index 59c5779de..20b0f501f 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -634,6 +634,7 @@ void CHECK_INCLUDE_DIAGRAM(const clanguml::config::config &config, #include "t20053/test_case.h" #include "t20054/test_case.h" #include "t20055/test_case.h" +#include "t20056/test_case.h" /// /// Package diagram tests diff --git a/tests/test_cases.yaml b/tests/test_cases.yaml index e623eb34b..ea21ac9fa 100644 --- a/tests/test_cases.yaml +++ b/tests/test_cases.yaml @@ -412,6 +412,8 @@ test_cases: - name: t20055 title: Test case for advanced filter in sequence diagram description: + - name: t20056 + title: Test case for option to fold repeated activities in sequence diagram Package diagrams: - name: t30001 title: Basic package diagram test case