diff --git a/src/cx/cursor.h b/src/cx/cursor.h index 8ff410ad..8e4edc63 100644 --- a/src/cx/cursor.h +++ b/src/cx/cursor.h @@ -126,6 +126,8 @@ public: return to_string(clang_getCursorKindSpelling(m_cursor.kind)); } + cursor definition() const { return clang_getCursorDefinition(m_cursor); } + bool is_definition() const { return clang_isCursorDefinition(m_cursor); } bool is_declaration() const { return clang_isDeclaration(kind()); } diff --git a/src/puml/class_diagram_generator.h b/src/puml/class_diagram_generator.h index 0a51074e..0701ef4c 100644 --- a/src/puml/class_diagram_generator.h +++ b/src/puml/class_diagram_generator.h @@ -90,12 +90,14 @@ public: return "-->"; case relationship_t::kInstantiation: return "..|>"; + case relationship_t::kFriendship: + return "<.."; default: return ""; } } - void generate(const class_ &c, std::ostream &ostr) const + void generate_aliases(const class_ &c, std::ostream &ostr) const { std::string class_type{"class"}; if (c.is_abstract()) @@ -104,6 +106,13 @@ public: ostr << class_type << " \"" << c.full_name(m_config.using_namespace); ostr << "\" as " << c.alias() << std::endl; + } + + void generate(const class_ &c, std::ostream &ostr) const + { + std::string class_type{"class"}; + if (c.is_abstract()) + class_type = "abstract"; ostr << class_type << " " << c.alias() << " {" << std::endl; @@ -166,7 +175,8 @@ public: destination = m_model.usr_to_name( m_config.using_namespace, r.destination); } - else if (r.destination.find("#") != std::string::npos) { + else if (r.destination.find("#") != std::string::npos || + r.destination.find("@") != std::string::npos) { destination = m_model.usr_to_name( m_config.using_namespace, r.destination); } @@ -204,6 +214,11 @@ public: { ostr << "@startuml" << std::endl; + for (const auto &c : m_model.classes) { + generate_aliases(c, ostr); + ostr << std::endl; + } + for (const auto &c : m_model.classes) { generate(c, ostr); ostr << std::endl; diff --git a/src/uml/class_diagram_model.h b/src/uml/class_diagram_model.h index 5953b26b..c747326d 100644 --- a/src/uml/class_diagram_model.h +++ b/src/uml/class_diagram_model.h @@ -44,7 +44,8 @@ enum class relationship_t { kContainment, kOwnership, kAssociation, - kInstantiation + kInstantiation, + kFriendship }; class element { diff --git a/src/uml/class_diagram_visitor.h b/src/uml/class_diagram_visitor.h index 56d020b7..86a1ffb6 100644 --- a/src/uml/class_diagram_visitor.h +++ b/src/uml/class_diagram_visitor.h @@ -201,6 +201,39 @@ static enum CXChildVisitResult enum_visitor( return ret; } +static enum CXChildVisitResult friend_class_visitor( + CXCursor cx_cursor, CXCursor cx_parent, CXClientData 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 friend class declaration{}: {} - {}:{}", + ctx->element.name, cursor.spelling(), cursor.kind()); + + enum CXChildVisitResult ret = CXChildVisit_Break; + switch (cursor.kind()) { + case CXCursor_TypeRef: { + spdlog::info("Adding friend declaration: {}, {}", cursor, + cursor.referenced()); + class_relationship r; + r.type = relationship_t::kFriendship; + r.label = "<>"; + r.destination = cursor.referenced().usr(); + + ctx->element.relationships.emplace_back(std::move(r)); + + ret = CXChildVisit_Continue; + } break; + default: + ret = CXChildVisit_Continue; + break; + } + + return ret; +} + static enum CXChildVisitResult class_visitor( CXCursor cx_cursor, CXCursor cx_parent, CXClientData client_data) { @@ -534,8 +567,12 @@ static enum CXChildVisitResult class_visitor( ctx->element.bases.emplace_back(std::move(cp)); ret = CXChildVisit_Continue; - break; - } + } break; + case CXCursor_FriendDecl: { + clang_visitChildren(cursor.get(), friend_class_visitor, ctx); + + ret = CXChildVisit_Continue; + } break; default: ret = CXChildVisit_Continue; break; diff --git a/tests/t00011/.clanguml b/tests/t00011/.clanguml new file mode 100644 index 00000000..69ef8449 --- /dev/null +++ b/tests/t00011/.clanguml @@ -0,0 +1,12 @@ +compilation_database_dir: .. +output_directory: puml +diagrams: + t00011_class: + type: class + glob: + - ../../tests/t00011/t00011.cc + using_namespace: + - clanguml::t00011 + include: + namespaces: + - clanguml::t00011 diff --git a/tests/t00011/t00011.cc b/tests/t00011/t00011.cc new file mode 100644 index 00000000..9aeec4cf --- /dev/null +++ b/tests/t00011/t00011.cc @@ -0,0 +1,18 @@ +namespace clanguml { +namespace t00011 { + +class B; + +class A { +private: + void foo() {} + friend class B; +}; + +class B { +public: + void foo() { m_a->foo(); } + A *m_a; +}; +} +} diff --git a/tests/t00011/test_case.h b/tests/t00011/test_case.h new file mode 100644 index 00000000..619efe90 --- /dev/null +++ b/tests/t00011/test_case.h @@ -0,0 +1,54 @@ +/** + * tests/t00011/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 t00011", "[unit-test]") +{ + spdlog::set_level(spdlog::level::debug); + + auto [config, db] = load_config("t00011"); + + auto diagram = config.diagrams["t00011_class"]; + + REQUIRE(diagram->name == "t00011_class"); + + REQUIRE(diagram->include.namespaces.size() == 1); + REQUIRE_THAT(diagram->include.namespaces, + VectorContains(std::string{"clanguml::t00011"})); + + REQUIRE(diagram->exclude.namespaces.size() == 0); + + REQUIRE(diagram->should_include("clanguml::t00011::A")); + REQUIRE(diagram->should_include("clanguml::t00011::B")); + + auto model = generate_class_diagram(db, diagram); + + REQUIRE(model.name == "t00011_class"); + + auto puml = generate_class_puml(diagram, model); + AliasMatcher _A(puml); + + REQUIRE_THAT(puml, StartsWith("@startuml")); + REQUIRE_THAT(puml, EndsWith("@enduml\n")); + REQUIRE_THAT(puml, IsClass(_A("A"))); + REQUIRE_THAT(puml, IsClass(_A("B"))); + + REQUIRE_THAT(puml, IsFriend(_A("A"), _A("B"))); + + save_puml( + "./" + config.output_directory + "/" + diagram->name + ".puml", puml); +} diff --git a/tests/test_cases.cc b/tests/test_cases.cc index dd9ce549..85a52fc7 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -105,6 +105,7 @@ using clanguml::test::matchers::IsClassTemplate; using clanguml::test::matchers::IsComposition; using clanguml::test::matchers::IsEnum; using clanguml::test::matchers::IsField; +using clanguml::test::matchers::IsFriend; using clanguml::test::matchers::IsInnerClass; using clanguml::test::matchers::IsInstantiation; using clanguml::test::matchers::Private; @@ -122,3 +123,4 @@ using clanguml::test::matchers::Static; #include "t00008/test_case.h" #include "t00009/test_case.h" #include "t00010/test_case.h" +#include "t00011/test_case.h" diff --git a/tests/test_cases.h b/tests/test_cases.h index dc283d09..750dfb4b 100644 --- a/tests/test_cases.h +++ b/tests/test_cases.h @@ -270,6 +270,13 @@ ContainsMatcher IsAssociation(std::string const &from, std::string const &to, fmt::format("{} --> {} : {}", from, to, label), caseSensitivity)); } +ContainsMatcher IsFriend(std::string const &from, std::string const &to, + CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes) +{ + return ContainsMatcher(CasedString( + fmt::format("{} <.. {} : <>", from, to), caseSensitivity)); +} + ContainsMatcher IsComposition(std::string const &from, std::string const &to, std::string const &label, CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes)