Added PlantUML links generation in class diagrams

This commit is contained in:
Bartek Kryza
2022-03-20 22:57:17 +01:00
parent 3c30350edc
commit d7e27841bc
15 changed files with 351 additions and 14 deletions

View File

@@ -20,6 +20,8 @@
#include "util/error.h"
#include <inja/inja.hpp>
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<diagram_config, diagram_model>::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<diagram_config, diagram_model>::generate_link(ostr, e);
}
if (!e.style().empty())
ostr << " " << e.style();

View File

@@ -59,6 +59,9 @@ class generator : public common_generator<diagram_config, diagram_model> {
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;

View File

@@ -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;
}
}

View File

@@ -18,12 +18,16 @@
#pragma once
#include "common/model/decorated_element.h"
#include "common/model/source_location.h"
#include <inja/inja.hpp>
#include <string>
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_;

View File

@@ -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<class_>(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));
}

View File

@@ -23,11 +23,13 @@
#include <cppast/libclang_parser.hpp>
#include <glob/glob.hpp>
#include <inja/inja.hpp>
#include <ostream>
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<std::string> &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 <typename E> inja::json element_context(const E &e) const;
private:
void init_context();
protected:
ConfigType &m_config;
DiagramType &m_model;
inja::json m_context;
};
template <typename C, typename D>
@@ -71,6 +84,50 @@ std::ostream &operator<<(std::ostream &os, const generator<C, D> &g)
return os;
}
template <typename C, typename D>
const inja::json &generator<C, D>::context() const
{
return m_context;
}
template <typename C, typename D> void generator<C, D>::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 <typename C, typename D>
template <typename E>
inja::json generator<C, D>::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 <typename C, typename D>
void generator<C, D>::generate_config_layout_hints(std::ostream &ostr) const
{
@@ -131,6 +188,28 @@ void generator<C, D>::generate_notes(
}
}
template <typename C, typename D>
void generator<C, D>::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 <typename DiagramModel, typename DiagramConfig,
typename DiagramVisitor>
DiagramModel generate(const cppast::libclang_compilation_database &db,

View File

@@ -73,6 +73,15 @@ const std::vector<relationship> &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);

View File

@@ -20,8 +20,11 @@
#include "decorated_element.h"
#include "namespace.h"
#include "relationship.h"
#include "source_location.h"
#include "util/util.h"
#include <inja/inja.hpp>
#include <atomic>
#include <exception>
#include <string>
@@ -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<relationship> relationships_;
type_safe::optional<source_location> location_;
static std::atomic_uint64_t m_nextId;
};

View File

@@ -0,0 +1,19 @@
/**
* src/common/model/source_location.cc
*
* Copyright (c) 2021-2022 Bartek Kryza <bkryza@gmail.com>
*
* 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"

View File

@@ -0,0 +1,46 @@
/**
* src/common/model/source_location.h
*
* Copyright (c) 2021-2022 Bartek Kryza <bkryza@gmail.com>
*
* 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 <string>
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};
};
}

View File

@@ -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<config>();
}
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<filter> {
template <> struct convert<generate_links_config> {
static bool decode(const Node &node, generate_links_config &rhs)
{
if (node["prefix"])
rhs.prefix = node["prefix"].as<decltype(rhs.prefix)>();
if (node["link"])
rhs.link = node["link"].as<decltype(rhs.link)>();
if (node["tooltip"])
rhs.tooltip = node["tooltip"].as<decltype(rhs.tooltip)>();
return true;
}
};
//
// git_config Yaml decoder
//
template <> struct convert<git_config> {
static bool decode(const Node &node, git_config &rhs)
{
if (node["branch"])
rhs.branch = node["branch"].as<decltype(rhs.branch)>();
if (node["revision"])
rhs.revision = node["revision"].as<decltype(rhs.revision)>();
if (node["commit"])
rhs.commit = node["commit"].as<decltype(rhs.commit)>();
if (node["toplevel"])
rhs.toplevel = node["toplevel"].as<decltype(rhs.toplevel)>();
return true;
}
@@ -461,6 +501,8 @@ template <typename T> 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<class_diagram> {
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<sequence_diagram> {
return false;
get_option(node, rhs.start_from);
get_option(node, rhs.generate_links);
return true;
}
@@ -511,7 +551,6 @@ template <> struct convert<package_diagram> {
return false;
get_option(node, rhs.layout);
get_option(node, rhs.generate_links);
return true;
}
@@ -564,6 +603,7 @@ template <> struct convert<config> {
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"];

View File

@@ -75,7 +75,15 @@ struct layout_hint {
using layout_hints = std::map<std::string, std::vector<layout_hint>>;
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<bool> generate_packages{"generate_packages", false};
option<generate_links_config> generate_links{"generate_links"};
option<git_config> 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 {

View File

@@ -20,6 +20,17 @@
#include <spdlog/spdlog.h>
void inject_diagram_options(std::shared_ptr<clanguml::config::diagram> 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<clanguml::config::config, cppast::libclang_compilation_database>
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<diagram_model,
diagram_config, diagram_visitor>(db, diagram->name,
dynamic_cast<clanguml::config::sequence_diagram &>(*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<diagram_model,
diagram_config, diagram_visitor>(
db, diagram->name, dynamic_cast<diagram_config &>(*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<diagram_model,
diagram_config, diagram_visitor>(
db, diagram->name, dynamic_cast<diagram_config &>(*diagram));

View File

@@ -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]")

View File

@@ -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