From e4d77db5c073b3a44011ae7ffe81a3f2530f8b1f Mon Sep 17 00:00:00 2001 From: Bartek Kryza Date: Sun, 28 Feb 2021 19:13:15 +0100 Subject: [PATCH] Added basic class relationship handling --- Makefile | 4 +- src/cx/cursor.h | 5 + src/cx/type.h | 54 +++++++++++ src/puml/class_diagram_generator.h | 7 +- src/uml/class_diagram_model.h | 1 + src/uml/class_diagram_visitor.h | 147 ++++++++++++++++------------- tests/t00005/.clanguml | 12 +++ tests/t00005/t00005.cc | 55 +++++++++++ tests/t00005/test_case.h | 79 ++++++++++++++++ tests/test_cases.cc | 5 + tests/test_cases.h | 24 +++++ 11 files changed, 324 insertions(+), 69 deletions(-) create mode 100644 tests/t00005/.clanguml create mode 100644 tests/t00005/t00005.cc create mode 100644 tests/t00005/test_case.h diff --git a/Makefile b/Makefile index 655d2f9c..cf4d4769 100644 --- a/Makefile +++ b/Makefile @@ -18,9 +18,11 @@ # This Makefile is just a handy wrapper around cmake # -# Specify LLVM version +# Specify preferred LLVM version for CMake LLVM_VERSION ?= 11 +.DEFAULT_GOAL := test + .PHONY: clean clean: rm -rf debug release diff --git a/src/cx/cursor.h b/src/cx/cursor.h index bb28cde2..a23fc891 100644 --- a/src/cx/cursor.h +++ b/src/cx/cursor.h @@ -118,6 +118,11 @@ public: CXCursorKind kind() const { return m_cursor.kind; } + std::string kind_spelling() const + { + return to_string(clang_getCursorKindSpelling(m_cursor.kind)); + } + bool is_definition() const { return clang_isCursorDefinition(m_cursor); } bool is_declaration() const { return clang_isDeclaration(kind()); } diff --git a/src/cx/type.h b/src/cx/type.h index ce6369b4..bfb1fab8 100644 --- a/src/cx/type.h +++ b/src/cx/type.h @@ -21,6 +21,8 @@ #include #include +#include "util/util.h" + namespace clanguml { namespace cx { @@ -93,6 +95,11 @@ public: CXTypeKind kind() const { return m_type.kind; } + std::string kind_spelling() + { + return to_string(clang_getTypeKindSpelling(m_type.kind)); + } + CXCallingConv calling_convention() const { return clang_getFunctionTypeCallingConv(m_type); @@ -116,6 +123,39 @@ public: bool is_pod() const { return clang_isPODType(m_type); } + bool is_pointer() const { return kind() == CXType_Pointer; } + + bool is_record() const { return kind() == CXType_Record; } + + /** + * @brief Return final referenced type. + * + * This method allows to extract a final type in case a type consists of a + * single or multiple pointers or references. + * + * @return Referenced type. + */ + type referenced() const + { + auto t = *this; + while (t.is_pointer() || t.is_reference()) { + t = t.pointee_type(); + } + + return t; + } + + bool is_reference() const + { + return (kind() == CXType_LValueReference) || + (kind() == CXType_RValueReference); + } + + bool is_relationship() const + { + return is_pointer() || is_record() || is_reference() || !is_pod(); + } + type element_type() const { return clang_getElementType(m_type); } long long element_count() const { return clang_getNumElements(m_type); } @@ -157,6 +197,20 @@ public: return clang_Type_getCXXRefQualifier(m_type); } + std::string unqualified() const + { + auto toks = clanguml::util::split(spelling(), " "); + const std::vector qualifiers = { + "static", "const", "volatile", "register", "mutable"}; + + while (toks.size() > 0 && + std::count(qualifiers.begin(), qualifiers.end(), toks.front())) { + toks.erase(toks.begin()); + } + + return fmt::format("{}", fmt::join(toks, " ")); + } + private: CXType m_type; }; diff --git a/src/puml/class_diagram_generator.h b/src/puml/class_diagram_generator.h index aca47e21..62fddc2c 100644 --- a/src/puml/class_diagram_generator.h +++ b/src/puml/class_diagram_generator.h @@ -79,6 +79,7 @@ public: std::string to_string(relationship_t r) const { switch (r) { + case relationship_t::kOwnership: case relationship_t::kComposition: return "*--"; case relationship_t::kAggregation: @@ -86,7 +87,7 @@ public: case relationship_t::kContainment: return "+--"; case relationship_t::kAssociation: - return "--"; + return "-->"; default: return ""; } @@ -139,7 +140,9 @@ public: if (m.is_static) ostr << "{static} "; - ostr << to_string(m.scope) << m.type << " " << m.name << std::endl; + ostr << to_string(m.scope) + << ns_relative(m_config.using_namespace, m.type) << " " + << m.name << std::endl; } ostr << "}" << std::endl; diff --git a/src/uml/class_diagram_model.h b/src/uml/class_diagram_model.h index 4f5de56b..bdd30134 100644 --- a/src/uml/class_diagram_model.h +++ b/src/uml/class_diagram_model.h @@ -33,6 +33,7 @@ namespace class_diagram { enum class scope_t { kPublic, kProtected, kPrivate }; enum class relationship_t { + kNone, kExtension, kComposition, kAggregation, diff --git a/src/uml/class_diagram_visitor.h b/src/uml/class_diagram_visitor.h index 7edce05c..01593b00 100644 --- a/src/uml/class_diagram_visitor.h +++ b/src/uml/class_diagram_visitor.h @@ -253,86 +253,101 @@ static enum CXChildVisitResult class_visitor( auto t = cursor.type(); class_member m; m.name = cursor.spelling(); - m.type = cursor.type().spelling(); + m.type = cursor.type().canonical().unqualified(); m.scope = cx_access_specifier_to_scope( cursor.cxxaccess_specifier()); m.is_static = cursor.is_static(); - spdlog::info("Adding member {} {}::{}", m.type, - ctx->element.name, cursor.spelling()); + spdlog::info("Adding member {} {}::{} (type kind: {} | {} " + "| {} | {})", + m.type, ctx->element.name, cursor.spelling(), + t.kind_spelling(), t.pointee_type().spelling(), + t.is_pod(), cursor.type().canonical().spelling()); - relationship_t relationship_type = - relationship_t::kOwnership; + relationship_t relationship_type = relationship_t::kNone; // 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; + if (t.is_relationship() && + config.should_include( + cursor.type().canonical().unqualified())) { + spdlog::info( + "Analazing possible relationship candidate: {}", + t.spelling()); + if (t.kind() == CXType_Record) { + spdlog::info( + "Found relationship candidate record: {} | {}", + t.spelling(), t.pointee_type().spelling()); + relationship_type = relationship_t::kOwnership; + } + else if (t.kind() == CXType_Pointer) { + spdlog::info( + "Found relationship candidate pointer: {}", + t.spelling()); + relationship_type = relationship_t::kAssociation; + t = t.referenced(); + } + else if (t.kind() == CXType_LValueReference) { + spdlog::info("Found relationship candidate " + "lvalue reference: {}", + t.spelling()); + relationship_type = relationship_t::kAssociation; + t = t.referenced(); + } + else if (t.kind() == CXType_RValueReference) { + spdlog::info("Found relationship candidate " + "rvalue reference: {}", + t.spelling()); + relationship_type = relationship_t::kOwnership; + t = t.referenced(); + } + + if (relationship_type != relationship_t::kNone) { + spdlog::info( + "Found unknown candidate: {}", t.spelling()); + spdlog::error("UNKNOWN CXTYPE: {}", t.kind()); + class_relationship r; + auto template_argument_count = + t.template_arguments_count(); + std::string name = t.canonical().unqualified(); + + 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 { - 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); + r.destination = name; + r.type = relationship_type; + r.label = m.name; + ctx->element.relationships.emplace_back( + std::move(r)); } - break; + + spdlog::info( + "Adding relationship to: {}", r.destination); } } diff --git a/tests/t00005/.clanguml b/tests/t00005/.clanguml new file mode 100644 index 00000000..d433a5d5 --- /dev/null +++ b/tests/t00005/.clanguml @@ -0,0 +1,12 @@ +compilation_database_dir: .. +output_directory: puml +diagrams: + t00005_class: + type: class + glob: + - ../../tests/t00005/t00005.cc + using_namespace: + - clanguml::t00005 + include: + namespaces: + - clanguml::t00005 diff --git a/tests/t00005/t00005.cc b/tests/t00005/t00005.cc new file mode 100644 index 00000000..ea482596 --- /dev/null +++ b/tests/t00005/t00005.cc @@ -0,0 +1,55 @@ +namespace clanguml { +namespace t00005 { +class A { +}; + +class B { +}; + +class C { +}; + +class D { +}; + +class E { +}; + +class F { +}; + +class G { +}; + +class H { +}; + +class I { +}; + +class J { +}; + +class K { +}; + +class R { +public: + int some_int; + int *some_int_pointer; + int **some_int_pointer_pointer; + int &some_int_reference; + A a; + B *b; + C &c; + const D *d; + const E &e{}; + F &&f; + G **g; + H ***h; + I *&i; + volatile J *j; + mutable K *k; +}; +} +} diff --git a/tests/t00005/test_case.h b/tests/t00005/test_case.h new file mode 100644 index 00000000..feed6641 --- /dev/null +++ b/tests/t00005/test_case.h @@ -0,0 +1,79 @@ +/** + * tests/t00005/test_case.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. + */ + +TEST_CASE("Test t00005", "[unit-test]") +{ + spdlog::set_level(spdlog::level::debug); + + auto [config, db] = load_config("t00005"); + + auto diagram = config.diagrams["t00005_class"]; + + REQUIRE(diagram->name == "t00005_class"); + + REQUIRE(diagram->include.namespaces.size() == 1); + REQUIRE_THAT(diagram->include.namespaces, + VectorContains(std::string{"clanguml::t00005"})); + + REQUIRE(diagram->exclude.namespaces.size() == 0); + + REQUIRE(diagram->should_include("clanguml::t00005::A")); + REQUIRE(diagram->should_include("clanguml::t00005::B")); + REQUIRE(diagram->should_include("clanguml::t00005::C")); + REQUIRE(diagram->should_include("clanguml::t00005::D")); + + auto model = generate_class_diagram(db, diagram); + + REQUIRE(model.name == "t00005_class"); + + auto puml = generate_class_puml(diagram, model); + + REQUIRE_THAT(puml, StartsWith("@startuml")); + REQUIRE_THAT(puml, EndsWith("@enduml\n")); + REQUIRE_THAT(puml, IsClass("A")); + REQUIRE_THAT(puml, IsClass("B")); + REQUIRE_THAT(puml, IsClass("C")); + REQUIRE_THAT(puml, IsClass("D")); + REQUIRE_THAT(puml, IsClass("E")); + REQUIRE_THAT(puml, IsClass("F")); + REQUIRE_THAT(puml, IsClass("G")); + REQUIRE_THAT(puml, IsClass("H")); + REQUIRE_THAT(puml, IsClass("I")); + REQUIRE_THAT(puml, IsClass("J")); + REQUIRE_THAT(puml, IsClass("K")); + REQUIRE_THAT(puml, IsClass("R")); + + REQUIRE_THAT(puml, IsField(Public("int some_int"))); + REQUIRE_THAT(puml, IsField(Public("int * some_int_pointer"))); + REQUIRE_THAT(puml, IsField(Public("int ** some_int_pointer_pointer"))); + + REQUIRE_THAT(puml, IsComposition("R", "A", "a")); + REQUIRE_THAT(puml, IsAssociation("R", "B", "b")); + REQUIRE_THAT(puml, IsAssociation("R", "C", "c")); + REQUIRE_THAT(puml, IsAssociation("R", "D", "d")); + REQUIRE_THAT(puml, IsAssociation("R", "E", "e")); + REQUIRE_THAT(puml, IsComposition("R", "F", "f")); + REQUIRE_THAT(puml, IsAssociation("R", "G", "g")); + REQUIRE_THAT(puml, IsAssociation("R", "H", "h")); + REQUIRE_THAT(puml, IsAssociation("R", "I", "i")); + REQUIRE_THAT(puml, IsAssociation("R", "J", "j")); + REQUIRE_THAT(puml, IsAssociation("R", "K", "k")); + + save_puml( + "./" + config.output_directory + "/" + diagram->name + ".puml", puml); +} diff --git a/tests/test_cases.cc b/tests/test_cases.cc index 4a2e6a6c..c8825fd7 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -96,9 +96,13 @@ using clanguml::test::matchers::Default; using clanguml::test::matchers::HasCall; using clanguml::test::matchers::HasCallWithResponse; using clanguml::test::matchers::IsAbstractClass; +using clanguml::test::matchers::IsAggregation; +using clanguml::test::matchers::IsAssociation; using clanguml::test::matchers::IsBaseClass; using clanguml::test::matchers::IsClass; +using clanguml::test::matchers::IsComposition; using clanguml::test::matchers::IsEnum; +using clanguml::test::matchers::IsField; using clanguml::test::matchers::IsInnerClass; using clanguml::test::matchers::Private; using clanguml::test::matchers::Protected; @@ -109,3 +113,4 @@ using clanguml::test::matchers::Static; #include "t00002/test_case.h" #include "t00003/test_case.h" #include "t00004/test_case.h" +#include "t00005/test_case.h" diff --git a/tests/test_cases.h b/tests/test_cases.h index 2003cbce..c19c35c7 100644 --- a/tests/test_cases.h +++ b/tests/test_cases.h @@ -222,6 +222,30 @@ ContainsMatcher IsInnerClass(std::string const &parent, CasedString(parent + " +-- " + inner, caseSensitivity)); } +ContainsMatcher IsAssociation(std::string const &from, std::string const &to, + std::string const &label, + CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes) +{ + return ContainsMatcher(CasedString( + fmt::format("{} --> {} : {}", from, to, label), caseSensitivity)); +} + +ContainsMatcher IsComposition(std::string const &from, std::string const &to, + std::string const &label, + CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes) +{ + return ContainsMatcher(CasedString( + fmt::format("{} *-- {} : {}", from, to, label), caseSensitivity)); +} + +ContainsMatcher IsAggregation(std::string const &from, std::string const &to, + std::string const &label, + CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes) +{ + return ContainsMatcher(CasedString( + fmt::format("{} o-- {} : {}", from, to, label), caseSensitivity)); +} + ContainsMatcher IsMethod(std::string const &name, CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes) {