diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a2b9aeb5..5e493a69 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,8 +10,12 @@ jobs: uses: actions/checkout@v2 with: submodules: recursive + - name: Install add-apt-repository + run: sudo apt-get install software-properties-common + - name: Add llvm repository + run: sudo add-apt-repository 'deb http://apt.llvm.org/focal/ llvm-toolchain-focal-12 main' && sudo apt update - name: Install deps - run: sudo apt-get install ccache cmake libyaml-cpp-dev libspdlog-dev clang-11 libclang-11-dev libclang-cpp11-dev + run: sudo apt-get install ccache cmake libyaml-cpp-dev libspdlog-dev clang-12 libclang-12-dev libclang-cpp12-dev - name: Build and unit test run: | make debug diff --git a/CMakeLists.txt b/CMakeLists.txt index f4303ef7..2cef89d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,7 @@ set(CLANG_UML_INSTALL_BIN_DIR ${PROJECT_SOURCE_DIR}/bin) set(UML_HEADERS_DIR ${PROJECT_SOURCE_DIR}/src/uml) -set(LLVM_PREFERRED_VERSION 11.0.0) +set(LLVM_PREFERRED_VERSION 12.0.0) message(STATUS "Checking for spdlog...") find_package(spdlog REQUIRED) diff --git a/README.md b/README.md index 44eb556f..0fefa914 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,15 @@ TODO ## Usage +### Generating compile commands database +`clang-uml` requires an up-to-date +[compile-commands.json](https://clang.llvm.org/docs/JSONCompilationDatabase.html) +file, containing the list of commands used for compiling the source code. +Nowadays, this file can be generated rather easily using multiple methods: + * For [CMake](https://cmake.org/) projects, simply invoke the `cmake` command + as `cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ...` + * For Make projects checkout [compiledb](https://github.com/nickdiego/compiledb) or [Bear](https://github.com/rizsotto/Bear) + ### Invocation ### Configuration file format and examples diff --git a/docs/test_cases/t00002.md b/docs/test_cases/t00002.md index 0132702c..8d704b8c 100644 --- a/docs/test_cases/t00002.md +++ b/docs/test_cases/t00002.md @@ -52,6 +52,29 @@ public: a->foo_c(); } +private: + std::vector as; +}; + +// +// NOTE: libclang fails on: +// +// class D : public virtual B, public virtual C { +// +class E : virtual public B, virtual public C { +public: + void foo_a() override + { + for (auto a : as) + a->foo_a(); + } + + void foo_c() override + { + for (auto a : as) + a->foo_c(); + } + private: std::vector as; }; diff --git a/docs/test_cases/t00002_class.png b/docs/test_cases/t00002_class.png index 6f06077b..3cc7eec7 100644 Binary files a/docs/test_cases/t00002_class.png and b/docs/test_cases/t00002_class.png differ diff --git a/docs/test_cases/t00003_class.png b/docs/test_cases/t00003_class.png index 36c68afe..94bc1313 100644 Binary files a/docs/test_cases/t00003_class.png and b/docs/test_cases/t00003_class.png differ diff --git a/docs/test_cases/t00004_class.png b/docs/test_cases/t00004_class.png index 6c8a47b2..70189fa3 100644 Binary files a/docs/test_cases/t00004_class.png and b/docs/test_cases/t00004_class.png differ diff --git a/docs/test_cases/t00005_class.png b/docs/test_cases/t00005_class.png index 9e5ce5f6..a5bb5e15 100644 Binary files a/docs/test_cases/t00005_class.png and b/docs/test_cases/t00005_class.png differ diff --git a/docs/test_cases/t00006_class.png b/docs/test_cases/t00006_class.png index 18d7ce5d..8a8534a7 100644 Binary files a/docs/test_cases/t00006_class.png and b/docs/test_cases/t00006_class.png differ diff --git a/docs/test_cases/t00007_class.png b/docs/test_cases/t00007_class.png index 875ae54e..b19d65ef 100644 Binary files a/docs/test_cases/t00007_class.png and b/docs/test_cases/t00007_class.png differ diff --git a/docs/test_cases/t00008_class.png b/docs/test_cases/t00008_class.png index 35255dee..b346dcef 100644 Binary files a/docs/test_cases/t00008_class.png and b/docs/test_cases/t00008_class.png differ diff --git a/docs/test_cases/t00009_class.png b/docs/test_cases/t00009_class.png index ce51c215..5d128990 100644 Binary files a/docs/test_cases/t00009_class.png and b/docs/test_cases/t00009_class.png differ diff --git a/docs/test_cases/t00010_class.png b/docs/test_cases/t00010_class.png index 5393151d..d2a4a194 100644 Binary files a/docs/test_cases/t00010_class.png and b/docs/test_cases/t00010_class.png differ diff --git a/docs/test_cases/t00011_class.png b/docs/test_cases/t00011_class.png index fdf63196..899a869f 100644 Binary files a/docs/test_cases/t00011_class.png and b/docs/test_cases/t00011_class.png differ diff --git a/docs/test_cases/t00012_class.png b/docs/test_cases/t00012_class.png index 0a5bfd89..7b498ee6 100644 Binary files a/docs/test_cases/t00012_class.png and b/docs/test_cases/t00012_class.png differ diff --git a/docs/test_cases/t00013_class.png b/docs/test_cases/t00013_class.png index 71a64e61..7bc2ee8c 100644 Binary files a/docs/test_cases/t00013_class.png and b/docs/test_cases/t00013_class.png differ diff --git a/docs/test_cases/t00014_class.png b/docs/test_cases/t00014_class.png index 6bbd48d2..fb507cec 100644 Binary files a/docs/test_cases/t00014_class.png and b/docs/test_cases/t00014_class.png differ diff --git a/docs/test_cases/t00015_class.png b/docs/test_cases/t00015_class.png index 00000fb5..6a26f0bb 100644 Binary files a/docs/test_cases/t00015_class.png and b/docs/test_cases/t00015_class.png differ diff --git a/docs/test_cases/t00016_class.png b/docs/test_cases/t00016_class.png index e6b2686b..fb75cb14 100644 Binary files a/docs/test_cases/t00016_class.png and b/docs/test_cases/t00016_class.png differ diff --git a/docs/test_cases/t20001_sequence.png b/docs/test_cases/t20001_sequence.png index 65779c1c..9a7894c9 100644 Binary files a/docs/test_cases/t20001_sequence.png and b/docs/test_cases/t20001_sequence.png differ diff --git a/docs/test_cases/t90000_class.png b/docs/test_cases/t90000_class.png index 9da1afdf..9f2a34be 100644 Binary files a/docs/test_cases/t90000_class.png and b/docs/test_cases/t90000_class.png differ diff --git a/src/main.cc b/src/main.cc index 98689173..640b8208 100644 --- a/src/main.cc +++ b/src/main.cc @@ -52,12 +52,15 @@ int main(int argc, const char *argv[]) std::string config_path{".clanguml"}; std::string compilation_database_dir{'.'}; + std::vector diagram_names{}; bool verbose{false}; app.add_option( "-c,--config", config_path, "Location of configuration file"); app.add_option("-d,--compile-database", compilation_database_dir, "Location of configuration file"); + app.add_option("-n,--diagram-name", diagram_names, + "List of diagram names to generate"); app.add_flag("-v,--verbose", verbose, "Verbose logging"); CLI11_PARSE(app, argc, argv); @@ -80,6 +83,13 @@ int main(int argc, const char *argv[]) cppast::libclang_compilation_database db2(config.compilation_database_dir); for (const auto &[name, diagram] : config.diagrams) { + // If there are any specific diagram names provided on the command line, + // and this diagram is not in that list - skip it + if (!diagram_names.empty() && + std::find(diagram_names.begin(), diagram_names.end(), name) == + diagram_names.end()) + continue; + using clanguml::config::class_diagram; using clanguml::config::sequence_diagram; diff --git a/src/puml/class_diagram_generator.h b/src/puml/class_diagram_generator.h index bd1d9441..63ee28d5 100644 --- a/src/puml/class_diagram_generator.h +++ b/src/puml/class_diagram_generator.h @@ -124,7 +124,7 @@ public: } } - void generate_aliases(const class_ &c, std::ostream &ostr) const + void generate_alias(const class_ &c, std::ostream &ostr) const { std::string class_type{"class"}; if (c.is_abstract()) @@ -135,6 +135,14 @@ public: ostr << "\" as " << c.alias() << std::endl; } + void generate_alias(const enum_ &e, std::ostream &ostr) const + { + ostr << "enum" + << " \"" << e.full_name(m_config.using_namespace); + + ostr << "\" as " << e.alias() << std::endl; + } + void generate(const class_ &c, std::ostream &ostr) const { std::string class_type{"class"}; @@ -199,54 +207,74 @@ public: if (m_config.should_include_relationship("inheritance")) for (const auto &b : c.bases) { - ostr << m_model.to_alias(m_config.using_namespace, - ns_relative(m_config.using_namespace, b.name)) - << " <|-- " - << m_model.to_alias(m_config.using_namespace, - ns_relative(m_config.using_namespace, c.name)) - << std::endl; + std::stringstream relstr; + try { + relstr << m_model.to_alias(m_config.using_namespace, + ns_relative(m_config.using_namespace, b.name)) + << " <|-- " + << m_model.to_alias(m_config.using_namespace, + ns_relative(m_config.using_namespace, c.name)) + << std::endl; + ostr << relstr.str(); + } + catch (error::uml_alias_missing &e) { + LOG_ERROR("Skipping inheritance relation from {} to {} due " + "to: {}", + b.name, c.name, e.what()); + } } for (const auto &r : c.relationships) { if (!m_config.should_include_relationship(name(r.type))) continue; - std::string destination; - 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); + std::stringstream relstr; - // If something went wrong and we have an empty destination - // generate the relationship but comment it out for - // debugging - if (destination.empty()) { - ostr << "' "; + std::string destination; + try { + 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); + + // If something went wrong and we have an empty destination + // generate the relationship but comment it out for + // debugging + if (destination.empty()) { + relstr << "' "; + destination = r.destination; + } + } + else { destination = r.destination; } + + relstr << m_model.to_alias(m_config.using_namespace, + ns_relative(m_config.using_namespace, + c.full_name(m_config.using_namespace))) + << " " << to_string(r.type) << " " + << m_model.to_alias(m_config.using_namespace, + ns_relative( + m_config.using_namespace, destination)); + + if (!r.label.empty()) + relstr << " : " << r.label; + + relstr << std::endl; + ostr << relstr.str(); } - else { - destination = r.destination; + catch (error::uml_alias_missing &e) { + LOG_ERROR("Skipping {} relation from {} to {} due " + "to: {}", + to_string(r.type), c.full_name(m_config.using_namespace), + destination, e.what()); } - - ostr << m_model.to_alias(m_config.using_namespace, - ns_relative(m_config.using_namespace, - c.full_name(m_config.using_namespace))) - << " " << to_string(r.type) << " " - << m_model.to_alias(m_config.using_namespace, - ns_relative(m_config.using_namespace, destination)); - - if (!r.label.empty()) - ostr << " : " << r.label; - - ostr << std::endl; } } void generate(const enum_ &e, std::ostream &ostr) const { - ostr << "enum " << ns_relative(m_config.using_namespace, e.name) << " {" - << std::endl; + ostr << "enum " << e.alias() << " {" << std::endl; for (const auto &enum_constant : e.constants) { ostr << enum_constant << std::endl; @@ -259,29 +287,39 @@ public: continue; std::string destination; - 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); - if (destination.empty()) { - ostr << "' "; + std::stringstream relstr; + try { + 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); + if (destination.empty()) { + relstr << "' "; + destination = r.destination; + } + } + else { destination = r.destination; } + + relstr << m_model.to_alias(m_config.using_namespace, + ns_relative(m_config.using_namespace, e.name)) + << " " << to_string(r.type) << " " + << m_model.to_alias(m_config.using_namespace, + ns_relative( + m_config.using_namespace, destination)); + + if (!r.label.empty()) + relstr << " : " << r.label; + + relstr << std::endl; + ostr << relstr.str(); } - else { - destination = r.destination; + catch (error::uml_alias_missing &ex) { + LOG_ERROR("Skipping {} relation from {} to {} due " + "to: {}", + to_string(r.type), e.name, destination, ex.what()); } - - ostr << m_model.to_alias(m_config.using_namespace, - ns_relative(m_config.using_namespace, e.name)) - << " " << to_string(r.type) << " " - << m_model.to_alias(m_config.using_namespace, - ns_relative(m_config.using_namespace, destination)); - - if (!r.label.empty()) - ostr << " : " << r.label; - - ostr << std::endl; } } @@ -294,7 +332,12 @@ public: if (m_config.should_include_entities("classes")) { for (const auto &c : m_model.classes) { - generate_aliases(c, ostr); + generate_alias(c, ostr); + ostr << std::endl; + } + + for (const auto &e : m_model.enums) { + generate_alias(e, ostr); ostr << std::endl; } diff --git a/src/uml/class_diagram_model.h b/src/uml/class_diagram_model.h index c381a372..674c5449 100644 --- a/src/uml/class_diagram_model.h +++ b/src/uml/class_diagram_model.h @@ -17,6 +17,7 @@ */ #pragma once +#include "util/error.h" #include "util/util.h" #include @@ -174,6 +175,13 @@ public: void add_relationship(class_relationship &&cr) { + if (cr.destination.empty()) { + LOG_WARN( + "Skipping relationship '{}' - {} - '{}' due empty destination", + cr.destination, to_string(cr.type), usr); + return; + } + auto it = std::find(relationships.begin(), relationships.end(), cr); if (it == relationships.end()) relationships.emplace_back(std::move(cr)); @@ -232,6 +240,17 @@ struct enum_ : public element { { return l.name == r.name; } + + std::string full_name( + const std::vector &using_namespaces) const + { + using namespace clanguml::util; + + std::ostringstream ostr; + ostr << ns_relative(using_namespaces, name); + + return ostr.str(); + } }; struct diagram { @@ -281,7 +300,14 @@ struct diagram { } } - return full_name; + for (const auto &e : enums) { + if (e.full_name(using_namespaces) == full_name) { + return e.alias(); + } + } + + throw error::uml_alias_missing( + fmt::format("Missing alias for {}", full_name)); } std::string usr_to_name(const std::vector &using_namespaces, diff --git a/src/uml/class_diagram_visitor.cc b/src/uml/class_diagram_visitor.cc index 7b7be600..731ab0dd 100644 --- a/src/uml/class_diagram_visitor.cc +++ b/src/uml/class_diagram_visitor.cc @@ -659,9 +659,12 @@ void tu_visitor::process_template_method( { class_method m; m.name = util::trim(mf.name()); - m.type = cppast::to_string( - static_cast(mf.function()) - .return_type()); + if (mf.function().kind() == cppast::cpp_entity_kind::constructor_t) + m.type = "void"; + else + m.type = cppast::to_string( + static_cast(mf.function()) + .return_type()); m.is_pure_virtual = false; m.is_virtual = false; m.is_const = cppast::is_const( @@ -747,16 +750,6 @@ void tu_visitor::process_function_parameter( // so we have to deduce the correct namespace prefix of the // template which is being instantiated mp.type = cppast::to_string(param.type()); - - auto &template_instantiation_type = - static_cast( - param_type); - auto &primary_template_entity = - template_instantiation_type.primary_template(); - - auto trawname = cppast::to_string(template_instantiation_type); - auto pte = cx::util::fully_prefixed(ctx.namespace_, - primary_template_entity.get(ctx.entity_index)[0].get()); } else { mp.type = cppast::to_string(param.type()); @@ -885,6 +878,13 @@ void tu_visitor::process_template_template_parameter( void tu_visitor::process_friend(const cppast::cpp_friend &f, class_ &parent) { + // Only process friends to other classes or class templates + if (!f.entity() || + (f.entity().value().kind() != cppast::cpp_entity_kind::class_t) && + (f.entity().value().kind() != + cppast::cpp_entity_kind::class_template_t)) + return; + class_relationship r; r.type = relationship_t::kFriendship; r.label = "<>"; diff --git a/src/util/error.h b/src/util/error.h new file mode 100644 index 00000000..661b7967 --- /dev/null +++ b/src/util/error.h @@ -0,0 +1,29 @@ +/** + * src/util/error.h + * + * 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. + */ +#pragma once + +#include + +namespace clanguml::error { +struct uml_alias_missing : public virtual std::runtime_error { + uml_alias_missing(const std::string &message) + : std::runtime_error(message) + { + } +}; +} diff --git a/src/util/util.h b/src/util/util.h index 574a70e2..f29e6858 100644 --- a/src/util/util.h +++ b/src/util/util.h @@ -34,6 +34,10 @@ std::string trim(const std::string &s); #define __FILENAME__ \ (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__) +#define LOG_ERROR(fmt__, ...) \ + spdlog::error(std::string("[{}:{}] ") + fmt__, __FILENAME__, __LINE__, \ + ##__VA_ARGS__) + #define LOG_WARN(fmt__, ...) \ spdlog::warn(std::string("[{}:{}] ") + fmt__, __FILENAME__, __LINE__, \ ##__VA_ARGS__) diff --git a/tests/t00002/t00002.cc b/tests/t00002/t00002.cc index b76abf07..6a8b92dd 100644 --- a/tests/t00002/t00002.cc +++ b/tests/t00002/t00002.cc @@ -33,6 +33,29 @@ public: a->foo_c(); } +private: + std::vector as; +}; + +// +// NOTE: libclang fails on: +// +// class D : public virtual B, public virtual C { +// +class E : virtual public B, virtual public C { +public: + void foo_a() override + { + for (auto a : as) + a->foo_a(); + } + + void foo_c() override + { + for (auto a : as) + a->foo_c(); + } + private: std::vector as; }; diff --git a/tests/t00004/test_case.h b/tests/t00004/test_case.h index 399ea1be..6508c5ed 100644 --- a/tests/t00004/test_case.h +++ b/tests/t00004/test_case.h @@ -46,10 +46,10 @@ TEST_CASE("t00004", "[test-case][class]") REQUIRE_THAT(puml, IsClass(_A("A"))); REQUIRE_THAT(puml, IsClass(_A("AA"))); REQUIRE_THAT(puml, IsClass(_A("AAA"))); - REQUIRE_THAT(puml, IsEnum("Lights")); + REQUIRE_THAT(puml, IsEnum(_A("Lights"))); REQUIRE_THAT(puml, IsInnerClass(_A("A"), _A("AA"))); REQUIRE_THAT(puml, IsInnerClass(_A("AA"), _A("AAA"))); - REQUIRE_THAT(puml, IsInnerClass(_A("AA"), "Lights")); + REQUIRE_THAT(puml, IsInnerClass(_A("AA"), _A("Lights"))); REQUIRE_THAT(puml, (IsMethod("foo"))); REQUIRE_THAT(puml, (IsMethod("foo2"))); diff --git a/thirdparty/cppast b/thirdparty/cppast index 466116ec..c294d891 160000 --- a/thirdparty/cppast +++ b/thirdparty/cppast @@ -1 +1 @@ -Subproject commit 466116ec1c5c610882d46eb60b8af9f64e6cdf1b +Subproject commit c294d8913a3e768a252fd123d2104c5fae37e1e2