From 1a4cf87ea268d466cabe696df9153557acb008ff Mon Sep 17 00:00:00 2001 From: Bartek Kryza Date: Fri, 26 Feb 2021 10:57:09 +0100 Subject: [PATCH] Added support for nested classes and enums --- src/cx/cursor.h | 18 ++ src/puml/class_diagram_generator.h | 21 +- src/uml/class_diagram_visitor.h | 312 +++++++++++++++++++---------- tests/CMakeLists.txt | 2 + tests/t00004/.clanguml | 17 ++ tests/t00004/t00004.cc | 17 ++ tests/test_cases.cc | 55 ++++- 7 files changed, 322 insertions(+), 120 deletions(-) create mode 100644 tests/t00004/.clanguml create mode 100644 tests/t00004/t00004.cc diff --git a/src/cx/cursor.h b/src/cx/cursor.h index 13cb9890..bb28cde2 100644 --- a/src/cx/cursor.h +++ b/src/cx/cursor.h @@ -69,15 +69,33 @@ public: return spelling() == "void"; } + /** + * @brief Return fully qualified cursor spelling + * + * This method generates a fully qualified name for the cursor by + * traversing the namespaces upwards. + * + * TODO: Add caching of this value. + * + * @return Fully qualified cursor spelling + */ std::string fully_qualified() const { std::list res; cursor iterator{m_cursor}; + if (iterator.spelling().empty()) + return {}; + + int limit = 100; while (iterator.kind() != CXCursor_TranslationUnit) { auto name = iterator.spelling(); if (!name.empty()) res.push_front(iterator.spelling()); iterator = iterator.semantic_parent(); + if (limit-- == 0) + throw std::runtime_error(fmt::format( + "Generating fully qualified name for '{}' failed at: '{}'", + spelling(), fmt::join(res, "::"))); } return fmt::format("{}", fmt::join(res, "::")); diff --git a/src/puml/class_diagram_generator.h b/src/puml/class_diagram_generator.h index 9e4b98a1..7bc0c6a0 100644 --- a/src/puml/class_diagram_generator.h +++ b/src/puml/class_diagram_generator.h @@ -99,7 +99,8 @@ public: else ostr << "class "; - ostr << c.name << " {" << std::endl; + ostr << namespace_relative(m_config.using_namespace, c.name) << " {" + << std::endl; // // Process methods @@ -148,18 +149,20 @@ public: } for (const auto &r : c.relationships) { - // only add UML relationships to classes in the diagram - if (m_config.has_class(r.destination)) - ostr << c.name << " " << to_string(r.type) << " " - << namespace_relative( - m_config.using_namespace, r.destination) - << " : " << r.label << std::endl; + ostr << namespace_relative(m_config.using_namespace, c.name) << " " + << to_string(r.type) << " " + << namespace_relative(m_config.using_namespace, r.destination); + if (!r.label.empty()) + ostr << " : " << r.label; + + ostr << std::endl; } } void generate(const enum_ &e, std::ostream &ostr) const { - ostr << "Enum " << e.name << " {" << std::endl; + ostr << "Enum " << namespace_relative(m_config.using_namespace, e.name) + << " {" << std::endl; for (const auto &enum_constant : e.constants) { ostr << enum_constant << std::endl; @@ -235,7 +238,7 @@ clanguml::model::class_diagram::diagram generate( spdlog::debug("Cursor name: {}", clang_getCString(clang_getCursorDisplayName(cursor))); - clanguml::visitor::class_diagram::tu_context ctx(d); + clanguml::visitor::class_diagram::tu_context ctx(d, diagram); auto res = clang_visitChildren(cursor, clanguml::visitor::class_diagram::translation_unit_visitor, &ctx); spdlog::debug("Processing result: {}", res); diff --git a/src/uml/class_diagram_visitor.h b/src/uml/class_diagram_visitor.h index 1ae65a18..8afe693f 100644 --- a/src/uml/class_diagram_visitor.h +++ b/src/uml/class_diagram_visitor.h @@ -42,33 +42,26 @@ using clanguml::model::class_diagram::enum_; using clanguml::model::class_diagram::relationship_t; using clanguml::model::class_diagram::scope_t; -template struct element_visitor_context { - element_visitor_context(T &e) - : element(e) - { - } - CXCursorKind current_cursor_kind; - std::vector namespace_; - - T &element; -}; - -struct class_visitor_context : element_visitor_context { - class_visitor_context(class_ &c) - : element_visitor_context(c) - { - } - scope_t scope; -}; - struct tu_context { - tu_context(diagram &d_) + tu_context(diagram &d_, const clanguml::config::class_diagram &config_) : d{d_} + , config{config_} { } std::vector namespace_; class_diagram::diagram &d; + const clanguml::config::class_diagram &config; +}; + +template struct element_visitor_context { + element_visitor_context(T &e) + : element(e) + { + } + tu_context *ctx; + + T &element; }; enum CXChildVisitResult visit_if_cursor_valid( @@ -113,22 +106,22 @@ scope_t cx_access_specifier_to_scope(CX_CXXAccessSpecifier as) static enum CXChildVisitResult enum_visitor( CXCursor cx_cursor, CXCursor cx_parent, CXClientData client_data) { - auto e = (struct enum_ *)client_data; + auto ctx = (element_visitor_context *)client_data; cx::cursor cursor{std::move(cx_cursor)}; cx::cursor parent{std::move(cx_parent)}; - spdlog::info("Visiting enum {}: {} - {}:{}", e->name, cursor.spelling(), - cursor.kind()); + spdlog::info("Visiting enum {}: {} - {}:{}", ctx->element.name, + cursor.spelling(), cursor.kind()); enum CXChildVisitResult ret = CXChildVisit_Break; switch (cursor.kind()) { case CXCursor_EnumConstantDecl: - visit_if_cursor_valid(cursor, [e](cx::cursor cursor) { - spdlog::info( - "Adding enum constant {}::{}", e->name, cursor.spelling()); + visit_if_cursor_valid(cursor, [ctx](cx::cursor cursor) { + spdlog::info("Adding enum constant {}::{}", ctx->element.name, + cursor.spelling()); - e->constants.emplace_back(cursor.spelling()); + ctx->element.constants.emplace_back(cursor.spelling()); }); ret = CXChildVisit_Continue; @@ -144,25 +137,94 @@ static enum CXChildVisitResult enum_visitor( static enum CXChildVisitResult class_visitor( CXCursor cx_cursor, CXCursor cx_parent, CXClientData client_data) { - auto c = (struct class_ *)client_data; + auto ctx = (element_visitor_context *)client_data; cx::cursor cursor{std::move(cx_cursor)}; cx::cursor parent{std::move(cx_parent)}; std::string cursor_name_str = cursor.spelling(); - spdlog::info("Visiting {}: {} - {}:{}", c->is_struct ? "struct" : "class", - c->name, cursor_name_str, cursor.kind()); + spdlog::info("Visiting {}: {} - {}:{}", + ctx->element.is_struct ? "struct" : "class", ctx->element.name, + cursor_name_str, cursor.kind()); + + auto &config = ctx->ctx->config; enum CXChildVisitResult ret = CXChildVisit_Break; bool is_constructor{false}; bool is_destructor{false}; + bool is_struct{false}; + bool is_vardecl{false}; switch (cursor.kind()) { + case CXCursor_StructDecl: + is_struct = true; + case CXCursor_ClassDecl: + case CXCursor_ClassTemplate: + if (!config.should_include(cursor.fully_qualified())) { + ret = CXChildVisit_Continue; + break; + } + + visit_if_cursor_valid(cursor, [ctx, is_struct](cx::cursor cursor) { + class_ c{}; + c.is_struct = is_struct; + c.name = cursor.fully_qualified(); + c.namespace_ = ctx->ctx->namespace_; + + auto class_ctx = element_visitor_context(c); + class_ctx.ctx = ctx->ctx; + + clang_visitChildren(cursor.get(), class_visitor, &class_ctx); + + class_relationship containment; + containment.type = relationship_t::kContainment; + containment.destination = c.name; + ctx->element.relationships.emplace_back(std::move(containment)); + + spdlog::info( + "Added relationship {} +-- {}", ctx->element.name, c.name); + + ctx->ctx->d.classes.emplace_back(std::move(c)); + }); + break; + case CXCursor_EnumDecl: + if (!config.should_include(cursor.fully_qualified())) { + ret = CXChildVisit_Continue; + break; + } + + visit_if_cursor_valid(cursor, [ctx, is_struct](cx::cursor cursor) { + enum_ e{}; + e.name = cursor.fully_qualified(); + e.namespace_ = ctx->ctx->namespace_; + + auto enum_ctx = element_visitor_context(e); + enum_ctx.ctx = ctx->ctx; + + clang_visitChildren(cursor.get(), enum_visitor, &enum_ctx); + + class_relationship containment; + containment.type = relationship_t::kContainment; + containment.destination = e.name; + ctx->element.relationships.emplace_back(std::move(containment)); + + spdlog::info( + "Added relationship {} +-- {}", ctx->element.name, e.name); + + ctx->ctx->d.enums.emplace_back(std::move(e)); + }); + ret = CXChildVisit_Continue; + break; case CXCursor_CXXMethod: case CXCursor_Constructor: case CXCursor_Destructor: case CXCursor_FunctionTemplate: { - visit_if_cursor_valid(cursor, [c](cx::cursor cursor) { + if (!config.should_include(cursor.fully_qualified())) { + ret = CXChildVisit_Continue; + break; + } + + visit_if_cursor_valid(cursor, [ctx](cx::cursor cursor) { class_method m; m.name = cursor.spelling(); m.type = cursor.type().result_type().spelling(); @@ -174,99 +236,116 @@ static enum CXChildVisitResult class_visitor( m.scope = cx_access_specifier_to_scope(cursor.cxxaccess_specifier()); - spdlog::info("Adding method {} {}::{}()", m.type, c->name, - cursor.spelling()); + spdlog::info("Adding method {} {}::{}()", m.type, + ctx->element.name, cursor.spelling()); - c->methods.emplace_back(std::move(m)); + ctx->element.methods.emplace_back(std::move(m)); }); ret = CXChildVisit_Continue; break; } case CXCursor_VarDecl: + is_vardecl = true; case CXCursor_FieldDecl: { - visit_if_cursor_valid(cursor, [c](cx::cursor cursor) { - auto t = cursor.type(); - class_member m; - m.name = cursor.spelling(); - m.type = cursor.type().spelling(); - m.scope = - cx_access_specifier_to_scope(cursor.cxxaccess_specifier()); - m.is_static = cursor.is_static(); + visit_if_cursor_valid( + cursor, [ctx, &config, is_vardecl](cx::cursor cursor) { + auto t = cursor.type(); + class_member m; + m.name = cursor.spelling(); + m.type = cursor.type().spelling(); + m.scope = cx_access_specifier_to_scope( + cursor.cxxaccess_specifier()); + m.is_static = cursor.is_static(); - spdlog::info("Adding member {} {}::{}", m.type, c->name, - cursor.spelling()); + spdlog::info("Adding member {} {}::{}", m.type, + ctx->element.name, cursor.spelling()); - relationship_t relationship_type = relationship_t::kOwnership; + relationship_t relationship_type = + relationship_t::kOwnership; - // Parse the field declaration to determine the relationship - // type - if (!t.is_pod()) { - while (true) { - if (t.kind() == CXType_Pointer) { - relationship_type = relationship_t::kAssociation; - t = t.pointee_type(); - continue; - } - else if (t.kind() == CXType_LValueReference) { - relationship_type = relationship_t::kAggregation; - t = t.pointee_type(); - continue; - } - else if (t.kind() == CXType_RValueReference) { - relationship_type = relationship_t::kAssociation; - t = t.pointee_type(); - continue; - } - else { - spdlog::error("UNKNOWN CXTYPE: {}", t.kind()); - class_relationship r; - auto template_argument_count = - t.template_arguments_count(); - std::string name = t.spelling(); - - if (template_argument_count > 0) { - std::vector template_arguments; - for (int i = 0; i < template_argument_count; - i++) { - auto tt = t.template_argument_type(i); - template_arguments.push_back(tt); - } - - if (name.rfind("vector") == 0 || - name.rfind("std::vector") == 0) { - r.type = relationship_t::kAggregation; - r.destination = - template_arguments[0].spelling(); - } - if (name.rfind("map") == 0 || - name.rfind("std::map") == 0) { - r.type = relationship_t::kAggregation; - r.destination = - template_arguments[1].spelling(); - } - r.label = m.name; - c->relationships.emplace_back(std::move(r)); + // Parse the field declaration to determine the relationship + // type + // Skip: + // - POD + // - function variables + if (!t.is_pod() && !is_vardecl && + config.should_include(cursor.type().spelling())) { + while (true) { + if (t.kind() == CXType_Pointer) { + relationship_type = + relationship_t::kAssociation; + t = t.pointee_type(); + continue; + } + else if (t.kind() == CXType_LValueReference) { + relationship_type = + relationship_t::kAggregation; + t = t.pointee_type(); + continue; + } + else if (t.kind() == CXType_RValueReference) { + relationship_type = + relationship_t::kAssociation; + t = t.pointee_type(); + continue; } else { - r.destination = name; - r.type = relationship_type; - r.label = m.name; - c->relationships.emplace_back(std::move(r)); + spdlog::error("UNKNOWN CXTYPE: {}", t.kind()); + class_relationship r; + auto template_argument_count = + t.template_arguments_count(); + std::string name = t.spelling(); + + if (template_argument_count > 0) { + std::vector template_arguments; + for (int i = 0; i < template_argument_count; + i++) { + auto tt = t.template_argument_type(i); + template_arguments.push_back(tt); + } + + if (name.rfind("vector") == 0 || + name.rfind("std::vector") == 0) { + r.type = relationship_t::kAggregation; + r.destination = + template_arguments[0].spelling(); + } + if (name.rfind("map") == 0 || + name.rfind("std::map") == 0) { + r.type = relationship_t::kAggregation; + r.destination = + template_arguments[1].spelling(); + } + r.label = m.name; + ctx->element.relationships.emplace_back( + std::move(r)); + } + else { + r.destination = name; + r.type = relationship_type; + r.label = m.name; + ctx->element.relationships.emplace_back( + std::move(r)); + } + + spdlog::debug("Adding relationship to: {}", + r.destination); } - - spdlog::debug( - "Adding relationship to: {}", r.destination); + break; } - break; } - } - c->members.emplace_back(std::move(m)); - }); + ctx->element.members.emplace_back(std::move(m)); + }); ret = CXChildVisit_Continue; break; } case CXCursor_CXXBaseSpecifier: { + if (!config.should_include(cursor.referenced().fully_qualified())) { + ret = CXChildVisit_Continue; + break; + } + auto ref_cursor = cursor.referenced(); auto display_name = ref_cursor.referenced().spelling(); @@ -292,7 +371,7 @@ static enum CXChildVisitResult class_visitor( cp.access = class_parent::access_t::kPublic; } - c->bases.emplace_back(std::move(cp)); + ctx->element.bases.emplace_back(std::move(cp)); ret = CXChildVisit_Continue; break; @@ -319,7 +398,6 @@ static enum CXChildVisitResult translation_unit_visitor( return CXChildVisit_Continue; } std::string cursor_name_str = cursor.spelling(); - spdlog::debug("Visiting cursor: {}", cursor_name_str); bool is_struct{false}; @@ -336,16 +414,23 @@ static enum CXChildVisitResult translation_unit_visitor( case CXCursor_ClassDecl: { spdlog::debug("Found class or class template declaration: {}", cursor_name_str); + if (!ctx->config.should_include(cursor.fully_qualified())) { + ret = CXChildVisit_Continue; + break; + } scope = scope_t::kPublic; visit_if_cursor_valid(cursor, [ctx, is_struct](cx::cursor cursor) { class_ c{}; c.is_struct = is_struct; - c.name = cursor.spelling(); + c.name = cursor.fully_qualified(); c.namespace_ = ctx->namespace_; - clang_visitChildren(cursor.get(), class_visitor, &c); + auto class_ctx = element_visitor_context(c); + class_ctx.ctx = ctx; + + clang_visitChildren(cursor.get(), class_visitor, &class_ctx); ctx->d.classes.emplace_back(std::move(c)); }); @@ -355,13 +440,20 @@ static enum CXChildVisitResult translation_unit_visitor( } case CXCursor_EnumDecl: { spdlog::debug("Found enum declaration: {}", cursor_name_str); + if (!ctx->config.should_include(cursor.fully_qualified())) { + ret = CXChildVisit_Continue; + break; + } visit_if_cursor_valid(cursor, [ctx, is_struct](cx::cursor cursor) { enum_ e{}; - e.name = cursor.spelling(); + e.name = cursor.fully_qualified(); e.namespace_ = ctx->namespace_; - clang_visitChildren(cursor.get(), enum_visitor, &e); + auto enum_ctx = element_visitor_context(e); + enum_ctx.ctx = ctx; + + clang_visitChildren(cursor.get(), enum_visitor, &enum_ctx); ctx->d.enums.emplace_back(std::move(e)); }); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5ec49275..5656778f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -9,6 +9,7 @@ set(CLANG_UML_TEST_CASES_SRC t00001/t00001.cc t00002/t00002.cc t00003/t00003.cc + t00004/t00004.cc ) set(CLANG_UML_TEST_CASES_HEADER catch.h @@ -27,5 +28,6 @@ target_link_libraries(test_cases configure_file(t00001/.clanguml t00001/.clanguml COPYONLY) configure_file(t00002/.clanguml t00002/.clanguml COPYONLY) configure_file(t00003/.clanguml t00003/.clanguml COPYONLY) +configure_file(t00004/.clanguml t00004/.clanguml COPYONLY) add_test(NAME test_cases COMMAND test_cases) diff --git a/tests/t00004/.clanguml b/tests/t00004/.clanguml new file mode 100644 index 00000000..81badcaf --- /dev/null +++ b/tests/t00004/.clanguml @@ -0,0 +1,17 @@ +compilation_database_dir: .. +output_directory: puml +diagrams: + t00004_class: + type: class + glob: + - ../../tests/t00004/t00004.cc + using_namespace: + - clanguml::t00004 + include: + namespaces: + - clanguml::t00004 + plantuml: + before: + - "' t00004 test class members" + after: + - 'note over "A": A class' diff --git a/tests/t00004/t00004.cc b/tests/t00004/t00004.cc new file mode 100644 index 00000000..b3a8d4d3 --- /dev/null +++ b/tests/t00004/t00004.cc @@ -0,0 +1,17 @@ +namespace clanguml { +namespace t00004 { + +class A { +public: + class AA { + public: + enum class Lights { Green, Yellow, Red }; + + class AAA { + }; + }; + + void foo() const {} +}; +} +} diff --git a/tests/test_cases.cc b/tests/test_cases.cc index 4a7721ae..715bf63d 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -1,3 +1,20 @@ +/** + * tests/test_cases.cc + * + * Copyright (c) 2021 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. + */ #define CATCH_CONFIG_MAIN #include "config/config.h" @@ -204,7 +221,43 @@ TEST_CASE("Test t00003", "[unit-test]") REQUIRE_THAT(puml, Contains("-int a")); REQUIRE_THAT(puml, Contains("-int b")); REQUIRE_THAT(puml, Contains("-int c")); - REQUIRE_THAT(puml, Contains("{static} +int static_int")); + + save_puml( + "./" + config.output_directory + "/" + diagram->name + ".puml", puml); +} + +TEST_CASE("Test t00004", "[unit-test]") +{ + spdlog::set_level(spdlog::level::debug); + + auto [config, db] = load_config("t00004"); + + auto diagram = config.diagrams["t00004_class"]; + + REQUIRE(diagram->name == "t00004_class"); + + REQUIRE(diagram->include.namespaces.size() == 1); + REQUIRE_THAT(diagram->include.namespaces, + VectorContains(std::string{"clanguml::t00004"})); + + REQUIRE(diagram->exclude.namespaces.size() == 0); + + REQUIRE(diagram->should_include("clanguml::t00004::A")); + REQUIRE(diagram->should_include("clanguml::t00004::A::AA")); + REQUIRE(diagram->should_include("clanguml::t00004::A:::AAA")); + + auto model = generate_class_diagram(db, diagram); + + REQUIRE(model.name == "t00004_class"); + + auto puml = generate_class_puml(diagram, model); + + REQUIRE_THAT(puml, StartsWith("@startuml")); + REQUIRE_THAT(puml, EndsWith("@enduml\n")); + REQUIRE_THAT(puml, Contains("class A")); + REQUIRE_THAT(puml, Contains("A +-- A::AA")); + REQUIRE_THAT(puml, Contains("A::AA +-- A::AA::AAA")); + REQUIRE_THAT(puml, Contains("A::AA +-- A::AA::Lights")); save_puml( "./" + config.output_directory + "/" + diagram->name + ".puml", puml);