diff --git a/src/common/model/enums.h b/src/common/model/enums.h index e9c3cd2e..4e09570c 100644 --- a/src/common/model/enums.h +++ b/src/common/model/enums.h @@ -39,7 +39,7 @@ enum class relationship_t { kDependency }; -enum class message_t { kCall, kReturn }; +enum class message_t { kCall, kReturn, kIf, kElse, kElseIf, kIfEnd }; std::string to_string(relationship_t r); diff --git a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc index ebf43302..c9450c0a 100644 --- a/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc +++ b/src/sequence_diagram/generators/plantuml/sequence_diagram_generator.cc @@ -113,37 +113,52 @@ void generator::generate_activity(const activity &a, std::ostream &ostr, std::vector &visited) const { for (const auto &m : a.messages) { - visited.push_back(m.from); + if (m.type == message_t::kCall) { + const auto &to = m_model.get_participant(m.to); + if (!to) + continue; - const auto &to = m_model.get_participant(m.to); - if (!to) - continue; + visited.push_back(m.from); - LOG_DBG("Generating message {} --> {}", m.from, m.to); + LOG_DBG("Generating message {} --> {}", m.from, m.to); - generate_call(m, ostr); + generate_call(m, ostr); - std::string to_alias = generate_alias(to.value()); + std::string to_alias = generate_alias(to.value()); - ostr << "activate " << to_alias << std::endl; + 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) == - visited.end()) { // break infinite recursion on recursive calls - LOG_DBG("Creating activity {} --> {} - missing sequence {}", - m.from, m.to, m.to); - generate_activity(m_model.sequences[m.to], ostr, visited); + if (m_model.sequences.find(m.to) != m_model.sequences.end()) { + if (std::find(visited.begin(), visited.end(), m.to) == + visited + .end()) { // break infinite recursion on recursive calls + LOG_DBG("Creating activity {} --> {} - missing sequence {}", + m.from, m.to, m.to); + generate_activity(m_model.sequences[m.to], ostr, visited); + } } + else + LOG_DBG("Skipping activity {} --> {} - missing sequence {}", + m.from, m.to, m.to); + + generate_return(m, ostr); + + ostr << "deactivate " << to_alias << std::endl; + + visited.pop_back(); + } + else if (m.type == message_t::kIf) { + ostr << "alt\n"; + } + else if (m.type == message_t::kElseIf) { + ostr << "else\n"; + } + else if (m.type == message_t::kElse) { + ostr << "else\n"; + } + else if (m.type == message_t::kIfEnd) { + ostr << "end\n"; } - else - LOG_DBG("Skipping activity {} --> {} - missing sequence {}", m.from, - m.to, m.to); - - generate_return(m, ostr); - - visited.pop_back(); - - ostr << "deactivate " << to_alias << std::endl; } } diff --git a/src/sequence_diagram/visitor/call_expression_context.h b/src/sequence_diagram/visitor/call_expression_context.h index 6feb6b34..68511876 100644 --- a/src/sequence_diagram/visitor/call_expression_context.h +++ b/src/sequence_diagram/visitor/call_expression_context.h @@ -70,6 +70,43 @@ struct call_expression_context { void leave_lambda_expression(); + clang::IfStmt *current_ifstmt() const + { + if (if_stmt_stack_.empty()) + return nullptr; + + return if_stmt_stack_.top(); + } + + void enter_ifstmt(clang::IfStmt *stmt) { return if_stmt_stack_.push(stmt); } + + void leave_ifstmt() + { + if (!if_stmt_stack_.empty()) { + if_stmt_stack_.pop(); + std::stack{}.swap(elseif_stmt_stack_); + } + } + + void enter_elseifstmt(clang::IfStmt *stmt) + { + return elseif_stmt_stack_.push(stmt); + } + + void leave_elseifstmt() + { + if (elseif_stmt_stack_.empty()) + return elseif_stmt_stack_.pop(); + } + + clang::IfStmt *current_elseifstmt() const + { + if (elseif_stmt_stack_.empty()) + return nullptr; + + return elseif_stmt_stack_.top(); + } + clang::CXXRecordDecl *current_class_decl_; clang::ClassTemplateDecl *current_class_template_decl_; clang::ClassTemplateSpecializationDecl @@ -83,6 +120,8 @@ struct call_expression_context { private: std::int64_t current_caller_id_; std::stack current_lambda_caller_id_; + std::stack if_stmt_stack_; + std::stack elseif_stmt_stack_; }; } \ No newline at end of file diff --git a/src/sequence_diagram/visitor/translation_unit_visitor.cc b/src/sequence_diagram/visitor/translation_unit_visitor.cc index bf5dec8f..dbd11cc7 100644 --- a/src/sequence_diagram/visitor/translation_unit_visitor.cc +++ b/src/sequence_diagram/visitor/translation_unit_visitor.cc @@ -539,6 +539,138 @@ bool translation_unit_visitor::TraverseCallExpr(clang::CallExpr *expr) return true; } +bool translation_unit_visitor::TraverseCompoundStmt(clang::CompoundStmt *stmt) +{ + using clanguml::common::model::message_t; + using clanguml::common::model::namespace_; + using clanguml::sequence_diagram::model::activity; + using clanguml::sequence_diagram::model::message; + + const auto *current_ifstmt = context().current_ifstmt(); + const auto *current_elseifstmt = context().current_elseifstmt(); + + // + // Add final else block (not else if) + // + if (current_elseifstmt != nullptr) { + if (current_elseifstmt->getElse() == stmt) { + const auto current_caller_id = context().caller_id(); + + if (current_caller_id) { + message m; + m.from = current_caller_id; + m.type = message_t::kElse; + + diagram().sequences[current_caller_id].messages.emplace_back( + std::move(m)); + } + } + } + else if(current_ifstmt != nullptr) { + if (current_ifstmt->getElse() == stmt) { + const auto current_caller_id = context().caller_id(); + + if (current_caller_id) { + message m; + m.from = current_caller_id; + m.type = message_t::kElse; + + diagram().sequences[current_caller_id].messages.emplace_back( + std::move(m)); + } + } + } + + RecursiveASTVisitor::TraverseCompoundStmt(stmt); + + return true; +} + +bool translation_unit_visitor::TraverseIfStmt(clang::IfStmt *stmt) +{ + using clanguml::common::model::message_t; + using clanguml::common::model::namespace_; + using clanguml::sequence_diagram::model::activity; + using clanguml::sequence_diagram::model::message; + + const auto *current_ifstmt = context().current_ifstmt(); + + context().enter_ifstmt(stmt); + + const auto current_caller_id = context().caller_id(); + + bool elseif_block{false}; + + if (current_caller_id) { + if (diagram().sequences.find(current_caller_id) == + diagram().sequences.end()) { + activity a; + a.from = current_caller_id; + diagram().sequences.insert({current_caller_id, std::move(a)}); + } + message m; + m.from = current_caller_id; + + // Check if this is a beginning of a new if statement, or an + // else if condition of the current if statement + if (current_ifstmt != nullptr) { + for (const auto *child_stmt : current_ifstmt->children()) { + if (child_stmt == stmt) { + elseif_block = true; + break; + } + } + } + + if (elseif_block) { + m.type = message_t::kElseIf; + context().enter_elseifstmt(stmt); + } + else { + m.type = message_t::kIf; + } + + diagram().sequences[current_caller_id].messages.emplace_back( + std::move(m)); + } + + RecursiveASTVisitor::TraverseIfStmt(stmt); + + context().leave_ifstmt(); + + if (current_caller_id && !elseif_block) { + message m; + m.from = current_caller_id; + m.type = message_t::kIfEnd; + + if (diagram().sequences.find(current_caller_id) != + diagram().sequences.end()) { + + auto ¤t_messages = + diagram().sequences[current_caller_id].messages; + // Remove the if/else messages if there were no calls + // added to the diagram between them + auto last_if_it = + std::find_if(current_messages.rbegin(), current_messages.rend(), + [](const message &m) { return m.type == message_t::kIf; }); + + bool last_if_block_is_empty = std::none_of( + current_messages.rbegin(), last_if_it, + [](const message &m) { return m.type == message_t::kCall; }); + + if (!last_if_block_is_empty) { + current_messages.emplace_back(std::move(m)); + } + else { + current_messages.erase( + (last_if_it + 1).base(), current_messages.end()); + } + } + } + + return true; +} + bool translation_unit_visitor::VisitCallExpr(clang::CallExpr *expr) { using clanguml::common::model::message_t; @@ -635,7 +767,6 @@ bool translation_unit_visitor::VisitCallExpr(clang::CallExpr *expr) } else { if (!process_function_call_expression(m, expr)) { - // expr->dump(); LOG_DBG("Skipping call to unsupported type of call expression " "at: {}", expr->getBeginLoc().printToString(source_manager())); @@ -825,6 +956,9 @@ bool translation_unit_visitor::process_function_call_expression( auto callee_name = callee_function->getQualifiedNameAsString() + "()"; + if (!diagram().should_include(callee_name)) + return false; + std::unique_ptr f_ptr; if (!get_unique_id(callee_function->getID()).has_value()) { diff --git a/src/sequence_diagram/visitor/translation_unit_visitor.h b/src/sequence_diagram/visitor/translation_unit_visitor.h index 61d3627a..82c99b4c 100644 --- a/src/sequence_diagram/visitor/translation_unit_visitor.h +++ b/src/sequence_diagram/visitor/translation_unit_visitor.h @@ -64,6 +64,10 @@ public: bool VisitFunctionTemplateDecl( clang::FunctionTemplateDecl *function_declaration); + bool TraverseCompoundStmt(clang::CompoundStmt *stmt); + + bool TraverseIfStmt(clang::IfStmt *stmt); + clanguml::sequence_diagram::model::diagram &diagram(); const clanguml::sequence_diagram::model::diagram &diagram() const; diff --git a/tests/t20020/.clang-uml b/tests/t20020/.clang-uml new file mode 100644 index 00000000..9f58d838 --- /dev/null +++ b/tests/t20020/.clang-uml @@ -0,0 +1,14 @@ +compilation_database_dir: .. +output_directory: puml +diagrams: + t20020_sequence: + type: sequence + glob: + - ../../tests/t20020/t20020.cc + include: + namespaces: + - clanguml::t20020 + using_namespace: + - clanguml::t20020 + start_from: + - function: "clanguml::t20020::tmain()" \ No newline at end of file diff --git a/tests/t20020/t20020.cc b/tests/t20020/t20020.cc new file mode 100644 index 00000000..df29e6b2 --- /dev/null +++ b/tests/t20020/t20020.cc @@ -0,0 +1,56 @@ +#include +#include + +namespace clanguml { +namespace t20020 { +struct A { + int a1() { return 0; } + int a2() { return 1; } + int a3() { return 2; } +}; + +struct B { + void log() { } + + int b1() { return 3; } + int b2() { return 4; } +}; + +int tmain() +{ + A a; + B b; + + int result{0}; + + if (reinterpret_cast(&a) % 100 == 0ULL) { + result = a.a1(); + } + else if (reinterpret_cast(&a) % 64 == 0ULL) { + if (a.a2() > 2) + result = b.b1(); + else + result = b.b2(); + } + else { + result = a.a3(); + } + + b.log(); + + // This if/else should not be included in the diagram at all + // as the calls to std will be excluded by the diagram filters + if (result != 2) { + result = std::exp(result); + } + else if(result == 3) { + result = 4; + } + else { + result = std::exp(result + 1); + } + + return result; +} +} +} \ No newline at end of file diff --git a/tests/t20020/test_case.h b/tests/t20020/test_case.h new file mode 100644 index 00000000..74437de4 --- /dev/null +++ b/tests/t20020/test_case.h @@ -0,0 +1,49 @@ +/** + * tests/t20020/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("t20020", "[test-case][sequence]") +{ + auto [config, db] = load_config("t20020"); + + auto diagram = config.diagrams["t20020_sequence"]; + + REQUIRE(diagram->name == "t20020_sequence"); + + auto model = generate_sequence_diagram(*db, diagram); + + REQUIRE(model->name() == "t20020_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("tmain()"), _A("A"), "a1()")); + REQUIRE_THAT(puml, HasCall(_A("tmain()"), _A("A"), "a2()")); + REQUIRE_THAT(puml, HasCall(_A("tmain()"), _A("A"), "a3()")); + + REQUIRE_THAT(puml, HasCall(_A("tmain()"), _A("B"), "b1()")); + REQUIRE_THAT(puml, HasCall(_A("tmain()"), _A("B"), "b2()")); + REQUIRE_THAT(puml, HasCall(_A("tmain()"), _A("B"), "log()")); + + 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 2e5944bb..f2b12c27 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -266,6 +266,7 @@ using namespace clanguml::test::matchers; #include "t20017/test_case.h" #include "t20018/test_case.h" #include "t20019/test_case.h" +#include "t20020/test_case.h" /// /// Package diagram tests diff --git a/tests/test_cases.yaml b/tests/test_cases.yaml index 20f5ba17..f94c1ef8 100644 --- a/tests/test_cases.yaml +++ b/tests/test_cases.yaml @@ -205,6 +205,9 @@ test_cases: - name: t20019 title: Curiously Recurring Template Pattern sequence diagram test case description: + - name: t20020 + title: If statement sequence diagram test case + description: Package diagrams: - name: t30001 title: Basic package diagram test case