From d7e27841bced0709b288b3736afdd1231095d883 Mon Sep 17 00:00:00 2001 From: Bartek Kryza Date: Sun, 20 Mar 2022 22:57:17 +0100 Subject: [PATCH] Added PlantUML links generation in class diagrams --- .../plantuml/class_diagram_generator.cc | 43 +++++++++- .../plantuml/class_diagram_generator.h | 3 + src/class_diagram/model/class_element.cc | 8 ++ src/class_diagram/model/class_element.h | 8 +- .../visitor/translation_unit_visitor.cc | 51 ++++++++++++ src/common/generators/plantuml/generator.h | 81 ++++++++++++++++++- src/common/model/element.cc | 9 +++ src/common/model/element.h | 8 +- src/common/model/source_location.cc | 19 +++++ src/common/model/source_location.h | 46 +++++++++++ src/config/config.cc | 50 ++++++++++-- src/config/config.h | 12 ++- tests/test_cases.cc | 17 ++++ tests/test_config.cc | 6 +- tests/test_config_data/simple.yml | 4 +- 15 files changed, 351 insertions(+), 14 deletions(-) create mode 100644 src/common/model/source_location.cc create mode 100644 src/common/model/source_location.h diff --git a/src/class_diagram/generators/plantuml/class_diagram_generator.cc b/src/class_diagram/generators/plantuml/class_diagram_generator.cc index 3d5e3ef1..50a8bf7a 100644 --- a/src/class_diagram/generators/plantuml/class_diagram_generator.cc +++ b/src/class_diagram/generators/plantuml/class_diagram_generator.cc @@ -20,6 +20,8 @@ #include "util/error.h" +#include + namespace clanguml::class_diagram::generators::plantuml { generator::generator(diagram_config &config, diagram_model &model) @@ -27,6 +29,27 @@ generator::generator(diagram_config &config, diagram_model &model) { } +void generator::generate_link( + std::ostream &ostr, const class_diagram::model::class_element &e) const +{ + if (e.file().empty()) + return; + + if (!m_config.generate_links().link.empty()) { + ostr << " [[["; + inja::render_to( + ostr, m_config.generate_links().link, element_context(e)); + } + + if (!m_config.generate_links().tooltip.empty()) { + ostr << "{"; + inja::render_to( + ostr, m_config.generate_links().tooltip, element_context(e)); + ostr << "}"; + } + ostr << "]]]"; +} + void generator::generate_alias(const class_ &c, std::ostream &ostr) const { std::string class_type{"class"}; @@ -72,6 +95,10 @@ void generator::generate( ostr << class_type << " " << c.alias(); + if (m_config.generate_links) { + common_generator::generate_link(ostr, c); + } + if (!c.style().empty()) ostr << " " << c.style(); @@ -124,6 +151,10 @@ void generator::generate( ostr << " : " << uns.relative(type); + if (m_config.generate_links) { + generate_link(ostr, m); + } + ostr << '\n'; } @@ -201,7 +232,13 @@ void generator::generate( ostr << "{static} "; ostr << plantuml_common::to_plantuml(m.scope()) << m.name() << " : " - << uns.relative(m.type()) << '\n'; + << uns.relative(m.type()); + + if (m_config.generate_links) { + generate_link(ostr, m); + } + + ostr << '\n'; } ostr << "}" << '\n'; @@ -233,6 +270,10 @@ void generator::generate( { ostr << "enum " << e.alias(); + if (m_config.generate_links) { + common_generator::generate_link(ostr, e); + } + if (!e.style().empty()) ostr << " " << e.style(); diff --git a/src/class_diagram/generators/plantuml/class_diagram_generator.h b/src/class_diagram/generators/plantuml/class_diagram_generator.h index cb570ca3..74119724 100644 --- a/src/class_diagram/generators/plantuml/class_diagram_generator.h +++ b/src/class_diagram/generators/plantuml/class_diagram_generator.h @@ -59,6 +59,9 @@ class generator : public common_generator { public: generator(diagram_config &config, diagram_model &model); + void generate_link( + std::ostream &ostr, const class_diagram::model::class_element &e) const; + void generate_alias(const class_ &c, std::ostream &ostr) const; void generate_alias(const enum_ &e, std::ostream &ostr) const; diff --git a/src/class_diagram/model/class_element.cc b/src/class_diagram/model/class_element.cc index de30ae5f..65688e14 100644 --- a/src/class_diagram/model/class_element.cc +++ b/src/class_diagram/model/class_element.cc @@ -34,4 +34,12 @@ std::string class_element::name() const { return name_; } std::string class_element::type() const { return type_; } +inja::json class_element::context() const +{ + inja::json ctx; + ctx["name"] = name(); + ctx["type"] = type(); + ctx["scope"] = to_string(scope()); + return ctx; +} } diff --git a/src/class_diagram/model/class_element.h b/src/class_diagram/model/class_element.h index a53fdeab..765fce87 100644 --- a/src/class_diagram/model/class_element.h +++ b/src/class_diagram/model/class_element.h @@ -18,12 +18,16 @@ #pragma once #include "common/model/decorated_element.h" +#include "common/model/source_location.h" + +#include #include namespace clanguml::class_diagram::model { -class class_element : public common::model::decorated_element { +class class_element : public common::model::decorated_element, + public common::model::source_location { public: class_element(common::model::scope_t scope, const std::string &name, const std::string &type); @@ -32,6 +36,8 @@ public: std::string name() const; std::string type() const; + virtual inja::json context() const; + private: common::model::scope_t scope_; std::string name_; diff --git a/src/class_diagram/visitor/translation_unit_visitor.cc b/src/class_diagram/visitor/translation_unit_visitor.cc index a36e6288..5cadce54 100644 --- a/src/class_diagram/visitor/translation_unit_visitor.cc +++ b/src/class_diagram/visitor/translation_unit_visitor.cc @@ -248,6 +248,11 @@ void translation_unit_visitor::process_namespace( p->set_name(e.name()); p->set_namespace(package_parent); + if (e.location().has_value()) { + p->set_file(e.location().value().file); + p->set_line(e.location().value().line); + } + if (ns_declaration.comment().has_value()) p->add_decorators( decorators::parse(ns_declaration.comment().value())); @@ -285,6 +290,11 @@ void translation_unit_visitor::process_enum_declaration( e.set_name(enm.name()); e.set_namespace(ctx.get_namespace()); + if (enm.location().has_value()) { + e.set_file(enm.location().value().file); + e.set_line(enm.location().value().line); + } + if (enm.comment().has_value()) e.add_decorators(decorators::parse(enm.comment().value())); @@ -326,6 +336,12 @@ void translation_unit_visitor::process_class_declaration( { auto c_ptr = std::make_unique(ctx.config().using_namespace()); auto &c = *c_ptr; + + if (cls.location().has_value()) { + c.set_file(cls.location().value().file); + c.set_line(cls.location().value().line); + } + c.is_struct(cls.class_kind() == cppast::cpp_class_kind::struct_t); c.set_name(cls.name()); @@ -741,6 +757,11 @@ void translation_unit_visitor::process_field( class_member m{ detail::cpp_access_specifier_to_scope(as), mv.name(), type_name}; + if (mv.location().has_value()) { + m.set_file(mv.location().value().file); + m.set_line(mv.location().value().line); + } + if (mv.comment().has_value()) m.add_decorators(decorators::parse(mv.comment().value())); @@ -830,6 +851,11 @@ void translation_unit_visitor::process_static_field( class_member m{detail::cpp_access_specifier_to_scope(as), mv.name(), cppast::to_string(mv.type())}; + if (mv.location().has_value()) { + m.set_file(mv.location().value().file); + m.set_line(mv.location().value().line); + } + m.is_static(true); if (mv.comment().has_value()) @@ -853,6 +879,11 @@ void translation_unit_visitor::process_method( m.is_defaulted(false); m.is_static(false); + if (mf.location().has_value()) { + m.set_file(mf.location().value().file); + m.set_line(mf.location().value().line); + } + if (mf.comment().has_value()) m.add_decorators(decorators::parse(mf.comment().value())); @@ -889,6 +920,11 @@ void translation_unit_visitor::process_template_method( m.is_defaulted(false); m.is_static(false); + if (mf.location().has_value()) { + m.set_file(mf.location().value().file); + m.set_line(mf.location().value().line); + } + if (mf.comment().has_value()) m.add_decorators(decorators::parse(mf.comment().value())); @@ -920,6 +956,11 @@ void translation_unit_visitor::process_static_method( m.is_defaulted(false); m.is_static(true); + if (mf.location().has_value()) { + m.set_file(mf.location().value().file); + m.set_line(mf.location().value().line); + } + if (mf.comment().has_value()) m.add_decorators(decorators::parse(mf.comment().value())); @@ -946,6 +987,11 @@ void translation_unit_visitor::process_constructor( m.is_defaulted(false); m.is_static(true); + if (mf.location().has_value()) { + m.set_file(mf.location().value().file); + m.set_line(mf.location().value().line); + } + if (mf.comment().has_value()) m.add_decorators(decorators::parse(mf.comment().value())); @@ -970,6 +1016,11 @@ void translation_unit_visitor::process_destructor( m.is_defaulted(false); m.is_static(true); + if (mf.location().has_value()) { + m.set_file(mf.location().value().file); + m.set_line(mf.location().value().line); + } + c.add_method(std::move(m)); } diff --git a/src/common/generators/plantuml/generator.h b/src/common/generators/plantuml/generator.h index 3d6f3309..bc289ee7 100644 --- a/src/common/generators/plantuml/generator.h +++ b/src/common/generators/plantuml/generator.h @@ -23,11 +23,13 @@ #include #include +#include #include namespace clanguml::common::generators::plantuml { +using clanguml::common::model::element; using clanguml::common::model::message_t; using clanguml::common::model::relationship_t; using clanguml::common::model::scope_t; @@ -42,6 +44,7 @@ public: : m_config{config} , m_model{model} { + init_context(); } virtual ~generator() = default; @@ -57,11 +60,21 @@ public: std::ostream &ostr, const std::vector &directives) const; void generate_notes( - std::ostream &ostr, const model::element &decorators) const; + std::ostream &ostr, const model::element &element) const; + + void generate_link(std::ostream &ostr, const model::element &e) const; + + const inja::json &context() const; + + template inja::json element_context(const E &e) const; + +private: + void init_context(); protected: ConfigType &m_config; DiagramType &m_model; + inja::json m_context; }; template @@ -71,6 +84,50 @@ std::ostream &operator<<(std::ostream &os, const generator &g) return os; } +template +const inja::json &generator::context() const +{ + return m_context; +} + +template void generator::init_context() +{ + if (m_config.git) { + m_context["git"]["branch"] = m_config.git().branch; + m_context["git"]["revision"] = m_config.git().revision; + m_context["git"]["commit"] = m_config.git().commit; + m_context["git"]["toplevel"] = m_config.git().toplevel; + } + + m_context["diagram"]["name"] = m_config.name; + m_context["diagram"]["type"] = to_string(m_config.type()); +} + +template +template +inja::json generator::element_context(const E &e) const +{ + auto ctx = context(); + + ctx["element"] = e.context(); + + if (!e.file().empty()) { + std::filesystem::path file{e.file()}; + std::string relative_path = std::filesystem::relative(file); + + if (ctx.template contains("git")) + relative_path = + std::filesystem::relative(file, ctx["git"]["toplevel"]); + + ctx["element"]["source"]["path"] = relative_path; + ctx["element"]["source"]["full_path"] = file.string(); + ctx["element"]["source"]["name"] = file.filename(); + ctx["element"]["source"]["line"] = e.line(); + } + + return ctx; +} + template void generator::generate_config_layout_hints(std::ostream &ostr) const { @@ -131,6 +188,28 @@ void generator::generate_notes( } } +template +void generator::generate_link( + std::ostream &ostr, const model::element &e) const +{ + if (e.file().empty()) + return; + + if (!m_config.generate_links().link.empty()) { + ostr << " [["; + inja::render_to( + ostr, m_config.generate_links().link, element_context(e)); + } + + if (!m_config.generate_links().tooltip.empty()) { + ostr << "{"; + inja::render_to( + ostr, m_config.generate_links().tooltip, element_context(e)); + ostr << "}"; + } + ostr << "]]"; +} + template DiagramModel generate(const cppast::libclang_compilation_database &db, diff --git a/src/common/model/element.cc b/src/common/model/element.cc index 310a4340..19d94063 100644 --- a/src/common/model/element.cc +++ b/src/common/model/element.cc @@ -73,6 +73,15 @@ const std::vector &element::relationships() const void element::append(const element &e) { decorated_element::append(e); } +inja::json element::context() const { + inja::json ctx; + ctx["name"] = name(); + ctx["alias"] = alias(); + ctx["full_name"] = full_name(false); + ctx["namespace"] = get_namespace().to_string(); + return ctx; +} + bool operator==(const element &l, const element &r) { return l.full_name(false) == r.full_name(false); diff --git a/src/common/model/element.h b/src/common/model/element.h index bb8985ba..3adeec0b 100644 --- a/src/common/model/element.h +++ b/src/common/model/element.h @@ -20,8 +20,11 @@ #include "decorated_element.h" #include "namespace.h" #include "relationship.h" +#include "source_location.h" #include "util/util.h" +#include + #include #include #include @@ -29,7 +32,7 @@ namespace clanguml::common::model { -class element : public decorated_element { +class element : public decorated_element, public source_location { public: element(const namespace_ &using_namespace); @@ -74,6 +77,8 @@ public: friend std::ostream &operator<<(std::ostream &out, const element &rhs); + virtual inja::json context() const; + protected: const uint64_t m_id{0}; @@ -82,6 +87,7 @@ private: namespace_ ns_; namespace_ using_namespace_; std::vector relationships_; + type_safe::optional location_; static std::atomic_uint64_t m_nextId; }; diff --git a/src/common/model/source_location.cc b/src/common/model/source_location.cc new file mode 100644 index 00000000..46de5482 --- /dev/null +++ b/src/common/model/source_location.cc @@ -0,0 +1,19 @@ +/** +* src/common/model/source_location.cc +* +* 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. + */ + +#include "source_location.h" diff --git a/src/common/model/source_location.h b/src/common/model/source_location.h new file mode 100644 index 00000000..6e2c4a91 --- /dev/null +++ b/src/common/model/source_location.h @@ -0,0 +1,46 @@ +/** + * src/common/model/source_location.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. + */ +#pragma once + +#include + +namespace clanguml::common::model { + +class source_location { +public: + source_location() = default; + + source_location(const std::string &f, unsigned int l) + : file_{f} + , line_{l} + { + } + + const std::string &file() const { return file_; } + + void set_file(const std::string &file) { file_ = file; } + + unsigned int line() const { return line_; } + + void set_line(const unsigned line) { line_ = line; } + +private: + std::string file_; + unsigned int line_{0}; +}; +} \ No newline at end of file diff --git a/src/config/config.cc b/src/config/config.cc index 548e2290..1fb31353 100644 --- a/src/config/config.cc +++ b/src/config/config.cc @@ -32,6 +32,19 @@ config load(const std::string &config_file) doc.force_insert( "__parent_path", config_file_path.parent_path().string()); + // If the current directory is also a git repository, + // load some config values which can be included in the + // generated diagrams + if(util::is_git_repository() && !doc["git"]) { + YAML::Node git_config{YAML::NodeType::Map}; + git_config["branch"] = util::get_git_branch(); + git_config["revision"] = util::get_git_revision(); + git_config["commit"] = util::get_git_commit(); + git_config["toplevel"] = util::get_git_toplevel_dir(); + + doc["git"] = git_config; + } + return doc.as(); } catch (YAML::BadFile &e) { @@ -92,6 +105,7 @@ void inheritable_diagram_options::inherit( puml.override(parent.puml); generate_method_arguments.override(parent.generate_method_arguments); generate_links.override(parent.generate_links); + git.override(parent.git); } bool diagram::should_include_entities(const std::string &ent) @@ -277,6 +291,7 @@ using clanguml::config::package_diagram; using clanguml::config::plantuml; using clanguml::config::sequence_diagram; using clanguml::config::source_location; +using clanguml::config::git_config; inline bool has_key(const YAML::Node &n, const std::string &key) { @@ -447,8 +462,33 @@ template <> struct convert { template <> struct convert { static bool decode(const Node &node, generate_links_config &rhs) { - if (node["prefix"]) - rhs.prefix = node["prefix"].as(); + if (node["link"]) + rhs.link = node["link"].as(); + + if (node["tooltip"]) + rhs.tooltip = node["tooltip"].as(); + + return true; + } +}; + +// +// git_config Yaml decoder +// +template <> struct convert { + static bool decode(const Node &node, git_config &rhs) + { + if (node["branch"]) + rhs.branch = node["branch"].as(); + + if (node["revision"]) + rhs.revision = node["revision"].as(); + + if (node["commit"]) + rhs.commit = node["commit"].as(); + + if (node["toplevel"]) + rhs.toplevel = node["toplevel"].as(); return true; } @@ -461,6 +501,8 @@ template bool decode_diagram(const Node &node, T &rhs) get_option(node, rhs.include); get_option(node, rhs.exclude); get_option(node, rhs.puml); + get_option(node, rhs.git); + get_option(node, rhs.generate_links); return true; } @@ -479,7 +521,6 @@ template <> struct convert { get_option(node, rhs.include_relations_also_as_members); get_option(node, rhs.generate_method_arguments); get_option(node, rhs.generate_packages); - get_option(node, rhs.generate_links); return true; } @@ -495,7 +536,6 @@ template <> struct convert { return false; get_option(node, rhs.start_from); - get_option(node, rhs.generate_links); return true; } @@ -511,7 +551,6 @@ template <> struct convert { return false; get_option(node, rhs.layout); - get_option(node, rhs.generate_links); return true; } @@ -564,6 +603,7 @@ template <> struct convert { get_option(node, rhs.generate_method_arguments); get_option(node, rhs.generate_packages); get_option(node, rhs.generate_links); + get_option(node, rhs.git); auto diagrams = node["diagrams"]; diff --git a/src/config/config.h b/src/config/config.h index 97493f3b..3ebabf55 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -75,7 +75,15 @@ struct layout_hint { using layout_hints = std::map>; struct generate_links_config { - std::string prefix; + std::string link; + std::string tooltip; +}; + +struct git_config { + std::string branch; + std::string revision; + std::string commit; + std::string toplevel; }; std::string to_string(const diagram_type t); @@ -93,6 +101,7 @@ struct inheritable_diagram_options { "generate_method_arguments", method_arguments::full}; option generate_packages{"generate_packages", false}; option generate_links{"generate_links"}; + option git{"git"}; void inherit(const inheritable_diagram_options &parent); }; @@ -123,7 +132,6 @@ struct diagram : public inheritable_diagram_options { bool should_include(const common::model::namespace_ &path) const; std::string name; -private: }; struct source_location { diff --git a/tests/test_cases.cc b/tests/test_cases.cc index 7f3216db..e8ab2337 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -20,6 +20,17 @@ #include +void inject_diagram_options(std::shared_ptr diagram) +{ + // Inject links config to all test cases + clanguml::config::generate_links_config links_config{ + "https://github.com/bkryza/clang-uml/blob/{{ git.commit }}/{{ " + "element.source.path }}#L{{ element.source.line }}", + "{{ element.name }}"}; + + diagram->generate_links.set(links_config); +} + std::pair load_config(const std::string &test_name) { @@ -50,6 +61,8 @@ clanguml::sequence_diagram::model::diagram generate_sequence_diagram( using diagram_visitor = clanguml::sequence_diagram::visitor::translation_unit_visitor; + inject_diagram_options(diagram); + auto model = clanguml::common::generators::plantuml::generate(db, diagram->name, dynamic_cast(*diagram)); @@ -66,6 +79,8 @@ clanguml::class_diagram::model::diagram generate_class_diagram( using diagram_visitor = clanguml::class_diagram::visitor::translation_unit_visitor; + inject_diagram_options(diagram); + auto model = clanguml::common::generators::plantuml::generate( db, diagram->name, dynamic_cast(*diagram)); @@ -82,6 +97,8 @@ clanguml::package_diagram::model::diagram generate_package_diagram( using diagram_visitor = clanguml::package_diagram::visitor::translation_unit_visitor; + inject_diagram_options(diagram); + return clanguml::common::generators::plantuml::generate( db, diagram->name, dynamic_cast(*diagram)); diff --git a/tests/test_config.cc b/tests/test_config.cc index 7ccc53d4..6fca0f3b 100644 --- a/tests/test_config.cc +++ b/tests/test_config.cc @@ -35,8 +35,10 @@ TEST_CASE("Test config simple", "[unit-test]") clanguml::config::method_arguments::full); CHECK(diagram.generate_packages() == true); CHECK(diagram.generate_links == true); - CHECK(diagram.generate_links().prefix == - "https://github.com/bkryza/clang-uml/blob/master/"); + CHECK(diagram.generate_links().link == + "https://github.com/bkryza/clang-uml/blob/{{ git.branch }}/{{ " + "element.source.file }}#L{{ element.source.line }}"); + CHECK(diagram.generate_links().tooltip == "{{ element.comment }}"); } TEST_CASE("Test config inherited", "[unit-test]") diff --git a/tests/test_config_data/simple.yml b/tests/test_config_data/simple.yml index 43d4c5e6..8e6d10a8 100644 --- a/tests/test_config_data/simple.yml +++ b/tests/test_config_data/simple.yml @@ -1,5 +1,6 @@ compilation_database_dir: debug output_directory: output + diagrams: class_main: type: class @@ -10,7 +11,8 @@ diagrams: generate_method_arguments: full generate_packages: true generate_links: - prefix: https://github.com/bkryza/clang-uml/blob/master/ + link: https://github.com/bkryza/clang-uml/blob/{{ git.branch }}/{{ element.source.file }}#L{{ element.source.line }} + tooltip: "{{ element.comment }}" include: namespaces: - clanguml