diff --git a/src/config/config.cc b/src/config/config.cc index a1309431..8349746b 100644 --- a/src/config/config.cc +++ b/src/config/config.cc @@ -101,6 +101,8 @@ void inheritable_diagram_options::inherit( base_directory.override(parent.base_directory); relative_to.override(parent.relative_to); comment_parser.override(parent.comment_parser); + combine_free_functions_into_file_participants.override( + combine_free_functions_into_file_participants); } std::string inheritable_diagram_options::simplify_template_type( @@ -589,6 +591,8 @@ template <> struct convert { return false; get_option(node, rhs.start_from); + get_option(node, rhs.combine_free_functions_into_file_participants); + get_option(node, rhs.relative_to); rhs.initialize_type_aliases(); diff --git a/src/config/config.h b/src/config/config.h index 202ffdec..77e18787 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -142,6 +142,8 @@ struct inheritable_diagram_options { option type_aliases{"type_aliases"}; option comment_parser{ "comment_parser", comment_parser_t::plain}; + option combine_free_functions_into_file_participants{ + "combine_free_functions_into_file_participants", false}; void inherit(const inheritable_diagram_options &parent); diff --git a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc index e2afa867..13fb3238 100644 --- a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc +++ b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc @@ -62,10 +62,25 @@ void generator::generate_call(const message &m, std::ostream &ostr) const message = dynamic_cast(to.value()) .message_name(model::function::message_render_mode::full); } + else if (m_config.combine_free_functions_into_file_participants()) { + if (to.value().type_name() == "function") { + message = + dynamic_cast(to.value()) + .message_name(model::function::message_render_mode::full); + } + else if (to.value().type_name() == "function_template") { + message = + dynamic_cast(to.value()) + .message_name(model::function::message_render_mode::full); + } + } - ostr << from.value().alias() << " " + const std::string from_alias = generate_alias(from.value()); + const std::string to_alias = generate_alias(to.value()); + + ostr << from_alias << " " << common::generators::plantuml::to_plantuml(message_t::kCall) << " " - << to.value().alias() << " : " << message << std::endl; + << to_alias << " : " << message << std::endl; LOG_DBG("Generated call '{}' from {} [{}] to {} [{}]", message, from, m.from, to, m.to); @@ -79,9 +94,13 @@ void generator::generate_return(const message &m, std::ostream &ostr) const const auto &from = m_model.get_participant(m.from); const auto &to = m_model.get_participant(m.to); - ostr << to.value().alias() << " " + const std::string from_alias = generate_alias(from.value()); + + const std::string to_alias = generate_alias(to.value()); + + ostr << to_alias << " " << common::generators::plantuml::to_plantuml(message_t::kReturn) - << " " << from.value().alias() << '\n'; + << " " << from_alias << '\n'; } } @@ -99,7 +118,9 @@ void generator::generate_activity(const activity &a, std::ostream &ostr, generate_call(m, ostr); - ostr << "activate " << to.value().alias() << std::endl; + std::string to_alias = generate_alias(to.value()); + + ostr << "activate " << to_alias << std::endl; if (m_model.sequences.find(m.to) != m_model.sequences.end()) { if (std::find(visited.begin(), visited.end(), m.to) == @@ -117,7 +138,7 @@ void generator::generate_activity(const activity &a, std::ostream &ostr, visited.pop_back(); - ostr << "deactivate " << to.value().alias() << std::endl; + ostr << "deactivate " << to_alias << std::endl; } } @@ -152,6 +173,35 @@ void generator::generate_participant(std::ostream &ostr, common::id_t id) const generated_participants_.emplace(class_id); } + else if ((participant.type_name() == "function" || + participant.type_name() == "function_template") && + m_config.combine_free_functions_into_file_participants()) { + // Create a single participant for all functions declared in a + // single file + const auto &file_path = + m_model.get_participant(participant_id) + .value() + .file(); + + assert(!file_path.empty()); + + const auto file_id = common::to_id(file_path); + + if (is_participant_generated(file_id)) + return; + + [[maybe_unused]] const auto &relative_to = + std::filesystem::canonical(m_config.relative_to()); + + auto participant_name = std::filesystem::relative( + std::filesystem::path{file_path}, relative_to) + .string(); + + ostr << "participant \"" << render_name(participant_name) + << "\" as " << fmt::format("C_{:022}", file_id) << '\n'; + + generated_participants_.emplace(file_id); + } else { ostr << "participant \"" << m_config.using_namespace().relative( @@ -192,6 +242,8 @@ void generator::generate(std::ostream &ostr) const break; } } + + // Use this to break out of recurrent loops std::vector visited_participants; @@ -199,20 +251,22 @@ void generator::generate(std::ostream &ostr) const m_model.get_participant(start_from); if (!from.has_value()) { - LOG_WARN( - "Failed to find participant {} for start_from condition", + LOG_WARN("Failed to find participant {} for start_from " + "condition", sf.location); continue; } generate_participant(ostr, start_from); - ostr << "activate " << from.value().alias() << std::endl; + std::string from_alias = generate_alias(from.value()); + + ostr << "activate " << from_alias << std::endl; generate_activity( m_model.sequences[start_from], ostr, visited_participants); - ostr << "deactivate " << from.value().alias() << std::endl; + ostr << "deactivate " << from_alias << std::endl; } else { // TODO: Add support for other sequence start location types @@ -225,4 +279,17 @@ void generator::generate(std::ostream &ostr) const ostr << "@enduml" << std::endl; } +std::string generator::generate_alias( + const model::participant &participant) const +{ + if ((participant.type_name() == "function" || + participant.type_name() == "function_template") && + m_config.combine_free_functions_into_file_participants()) { + const auto file_id = common::to_id(participant.file()); + + return fmt::format("C_{:022}", file_id); + } + + return participant.alias(); +} } diff --git a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.h b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.h index 98273676..23a4cbe7 100644 --- a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.h +++ b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.h @@ -66,6 +66,7 @@ private: std::string render_name(std::string name) const; mutable std::set generated_participants_; + std::string generate_alias(const model::participant &participant) const; }; } diff --git a/src/sequence_diagram/model/participant.h b/src/sequence_diagram/model/participant.h index 5a36411d..e633b7d2 100644 --- a/src/sequence_diagram/model/participant.h +++ b/src/sequence_diagram/model/participant.h @@ -246,6 +246,21 @@ struct function_template : public function, public template_trait { std::string full_name(bool relative = true) const override; std::string full_name_no_ns() const override; + + std::string message_name(message_render_mode mode) const override + { + std::ostringstream s; + render_template_params(s, using_namespace(), true); + std::string template_params = s.str(); + + if (mode == message_render_mode::no_arguments) { + return fmt::format("{}{}(){}", name(), template_params, + is_const() ? " const" : ""); + } + + return fmt::format("{}{}({}){}", name(), template_params, + fmt::join(parameters(), ","), is_const() ? " const" : ""); + } }; } diff --git a/src/sequence_diagram/visitor/translation_unit_visitor.cc b/src/sequence_diagram/visitor/translation_unit_visitor.cc index d97096e0..0f7bbf2b 100644 --- a/src/sequence_diagram/visitor/translation_unit_visitor.cc +++ b/src/sequence_diagram/visitor/translation_unit_visitor.cc @@ -376,6 +376,8 @@ bool translation_unit_visitor::VisitFunctionDecl(clang::FunctionDecl *f) set_unique_id(f->getID(), f_ptr->id()); + set_source_location(*f, *f_ptr); + // TODO: Handle overloaded functions with different arguments diagram().add_participant(std::move(f_ptr)); } @@ -401,6 +403,8 @@ bool translation_unit_visitor::VisitFunctionDecl(clang::FunctionDecl *f) set_unique_id(f->getID(), f_ptr->id()); + set_source_location(*f, *f_ptr); + // TODO: Handle overloaded functions with different arguments diagram().add_participant(std::move(f_ptr)); } @@ -437,6 +441,8 @@ bool translation_unit_visitor::VisitFunctionTemplateDecl( f_ptr->set_id(common::to_id(f_ptr->full_name(false))); + set_source_location(*function_template, *f_ptr); + context().update(function_template); context().set_caller_id(f_ptr->id()); @@ -784,8 +790,9 @@ bool translation_unit_visitor::process_class_template_method_call_expression( get_unique_id(template_declaration->getID()).value()); } else { - LOG_WARN("Cannot generate call due to unresolvable " - "CXXDependentScopeMemberExpr"); + LOG_DBG("Skipping call due to unresolvable " + "CXXDependentScopeMemberExpr at {}", + expr->getBeginLoc().printToString(source_manager())); } return true; diff --git a/tests/t20017/.clang-uml b/tests/t20017/.clang-uml new file mode 100644 index 00000000..0aa577ba --- /dev/null +++ b/tests/t20017/.clang-uml @@ -0,0 +1,16 @@ +compilation_database_dir: .. +output_directory: puml +diagrams: + t20017_sequence: + type: sequence + combine_free_functions_into_file_participants: true + relative_to: ../../tests/t20017 + glob: + - ../../tests/t20017/t20017.cc + include: + namespaces: + - clanguml::t20017 + using_namespace: + - clanguml::t20017 + start_from: + - function: "clanguml::t20017::tmain()" \ No newline at end of file diff --git a/tests/t20017/include/t20017_a.h b/tests/t20017/include/t20017_a.h new file mode 100644 index 00000000..49ed884b --- /dev/null +++ b/tests/t20017/include/t20017_a.h @@ -0,0 +1,9 @@ +#pragma once + +namespace clanguml { +namespace t20017 { +int a1(int x, int y) { return x + y; } +int a2(int x, int y) { return x - y; } +int a3(int x, int y) { return x * y; } +} +} \ No newline at end of file diff --git a/tests/t20017/include/t20017_b.h b/tests/t20017/include/t20017_b.h new file mode 100644 index 00000000..86c67f72 --- /dev/null +++ b/tests/t20017/include/t20017_b.h @@ -0,0 +1,9 @@ +#pragma once + +namespace clanguml { +namespace t20017 { +int b1(int x, int y); + +template T b2(T x, T y) { return x / y; } +} +} \ No newline at end of file diff --git a/tests/t20017/t20017.cc b/tests/t20017/t20017.cc new file mode 100644 index 00000000..ec6227de --- /dev/null +++ b/tests/t20017/t20017.cc @@ -0,0 +1,8 @@ +#include "include/t20017_a.h" +#include "include/t20017_b.h" + +namespace clanguml { +namespace t20017 { +int tmain() { return b2(a1(a2(a3(1, 2), b1(3, 4)), 5), 6); } +} +} \ No newline at end of file diff --git a/tests/t20017/t20017_b.cc b/tests/t20017/t20017_b.cc new file mode 100644 index 00000000..d0b80bb7 --- /dev/null +++ b/tests/t20017/t20017_b.cc @@ -0,0 +1,7 @@ +#include "include/t20017_b.h" + +namespace clanguml { +namespace t20017 { +int b1(int x, int y) { return x - y; } +} +} \ No newline at end of file diff --git a/tests/t20017/test_case.h b/tests/t20017/test_case.h new file mode 100644 index 00000000..94c4bf1a --- /dev/null +++ b/tests/t20017/test_case.h @@ -0,0 +1,51 @@ +/** + * tests/t20017/test_case.h + * + * Copyright (c) 2021-2022 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("t20017", "[test-case][sequence]") +{ + auto [config, db] = load_config("t20017"); + + auto diagram = config.diagrams["t20017_sequence"]; + + REQUIRE(diagram->name == "t20017_sequence"); + + auto model = generate_sequence_diagram(*db, diagram); + + REQUIRE(model->name() == "t20017_sequence"); + + auto puml = generate_sequence_puml(diagram, *model); + AliasMatcher _A(puml); + + REQUIRE_THAT(puml, StartsWith("@startuml")); + REQUIRE_THAT(puml, EndsWith("@enduml\n")); + + // Check if all calls exist + REQUIRE_THAT(puml, + HasCall(_A("t20017.cc"), _A("include/t20017_a.h"), "a1(int,int)")); + REQUIRE_THAT( + puml, HasCall(_A("t20017.cc"), _A("include/t20017_a.h"), "a2(int,int)")); + REQUIRE_THAT(puml, + HasCall(_A("t20017.cc"), _A("include/t20017_a.h"), "a3(int,int)")); + REQUIRE_THAT(puml, + HasCall(_A("t20017.cc"), _A("include/t20017_b.h"), "b1(int,int)")); + REQUIRE_THAT(puml, + HasCall(_A("t20017.cc"), _A("include/t20017_b.h"), "b2(int,int)")); + + save_puml( + "./" + config.output_directory() + "/" + diagram->name + ".puml", puml); +} \ No newline at end of file diff --git a/tests/test_cases.cc b/tests/test_cases.cc index bff8d7be..80695a32 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -263,6 +263,7 @@ using namespace clanguml::test::matchers; #include "t20014/test_case.h" #include "t20015/test_case.h" #include "t20016/test_case.h" +#include "t20017/test_case.h" /// /// Package diagram tests diff --git a/tests/test_cases.yaml b/tests/test_cases.yaml index e0b10242..4651bcda 100644 --- a/tests/test_cases.yaml +++ b/tests/test_cases.yaml @@ -196,6 +196,9 @@ test_cases: - name: t20016 title: Template method specialization sequence diagram test case description: + - name: t20017 + title: Test case for combine_free_functions_into_file_participants option + description: Package diagrams: - name: t30001 title: Basic package diagram test case