Merge pull request #5 from bkryza/add-first-version-of-inline-directive-support

Add first version of inline directive support
This commit is contained in:
Bartek Kryza
2021-07-31 21:07:37 +02:00
committed by GitHub
36 changed files with 1784 additions and 105 deletions

View File

@@ -26,6 +26,10 @@
* [t00025](./test_cases/t00025.md) - Template proxy pattern * [t00025](./test_cases/t00025.md) - Template proxy pattern
* [t00026](./test_cases/t00026.md) - Template memento pattern * [t00026](./test_cases/t00026.md) - Template memento pattern
* [t00027](./test_cases/t00027.md) - Template decorator pattern * [t00027](./test_cases/t00027.md) - Template decorator pattern
* [t00028](./test_cases/t00028.md) - PlantUML note decorator test case
* [t00029](./test_cases/t00029.md) - PlantUML skip decorator test case
* [t00030](./test_cases/t00030.md) - PlantUML relationship decorators test case
* [t00031](./test_cases/t00031.md) - PlantUML style decorator test case
## Sequence diagrams ## Sequence diagrams
* [t20001](./test_cases/t20001.md) - Basic sequence diagram * [t20001](./test_cases/t20001.md) - Basic sequence diagram
## Configuration diagrams ## Configuration diagrams

80
docs/test_cases/t00028.md Normal file
View File

@@ -0,0 +1,80 @@
# t00028 - PlantUML note decorator test case
## Config
```yaml
compilation_database_dir: ..
output_directory: puml
diagrams:
t00028_class:
type: class
glob:
- ../../tests/t00028/t00028.cc
using_namespace:
- clanguml::t00028
include:
namespaces:
- clanguml::t00028
```
## Source code
File t00028.cc
```cpp
#include <memory>
#include <vector>
namespace clanguml {
namespace t00028 {
/// \uml{note[top] A class note.}
class A {
};
/// \uml{note[] B class note.}
class B {
};
///
/// @uml{note:t00028_class[bottom] C class note.}
/// This is class C.
class C {
};
/// \uml{note
/// D
/// class
/// note.}
class D {
};
/// \uml{note E template class note.}
template <typename T> class E {
T param;
};
/// \uml{note:other_diagram[left] G class note.}
class G {
};
/// @uml{note[ bottom ] F enum note.}
enum class F { one, two, three };
/// \uml{note[right] R class note.}
class R {
A aaa;
B *bbb;
C &ccc;
std::vector<std::shared_ptr<D>> ddd;
E<int> eee;
G **ggg;
};
} // namespace t00028
} // namespace clanguml
```
## Generated UML diagrams
![t00028_class](./t00028_class.png "PlantUML note decorator test case")

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

77
docs/test_cases/t00029.md Normal file
View File

@@ -0,0 +1,77 @@
# t00029 - PlantUML skip decorator test case
## Config
```yaml
compilation_database_dir: ..
output_directory: puml
diagrams:
t00029_class:
type: class
glob:
- ../../tests/t00029/t00029.cc
using_namespace:
- clanguml::t00029
include:
namespaces:
- clanguml::t00029
```
## Source code
File t00029.cc
```cpp
#include <memory>
#include <vector>
namespace clanguml {
namespace t00029 {
class A {
};
/// \uml{skip}
class B {
};
template <typename T> class C {
T param;
};
/// @uml{skip:t00029_class}
template <typename T> class D {
T param;
};
enum class E { one, two, three };
/// \uml{skip}
enum class F { red, green, blue };
class G1 {
};
class G2 {
};
class G3 {
};
class G4 {
};
struct R {
G1 g1;
/// \uml{skip}
G2 g2;
/// \uml{skiprelationship}
G3 &g3;
std::shared_ptr<G4> g4;
};
} // namespace t00029
} // namespace clanguml
```
## Generated UML diagrams
![t00029_class](./t00029_class.png "PlantUML skip decorator test case")

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

58
docs/test_cases/t00030.md Normal file
View File

@@ -0,0 +1,58 @@
# t00030 - PlantUML relationship decorators test case
## Config
```yaml
compilation_database_dir: ..
output_directory: puml
diagrams:
t00030_class:
type: class
glob:
- ../../tests/t00030/t00030.cc
using_namespace:
- clanguml::t00030
include:
namespaces:
- clanguml::t00030
```
## Source code
File t00030.cc
```cpp
#include <memory>
#include <vector>
namespace clanguml {
namespace t00030 {
class A {
};
class B {
};
class C {
};
class D {
};
struct R {
/// @uml{association[]}
A aaa;
/// @uml{composition[0..1:1..*]}
std::vector<B> bbb;
/// @uml{aggregation[0..1:1..5]}
std::vector<C> ccc;
/// @uml{association[:1]}
D ddd;
};
} // namespace t00030
} // namespace clanguml
```
## Generated UML diagrams
![t00030_class](./t00030_class.png "PlantUML relationship decorators test case")

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

62
docs/test_cases/t00031.md Normal file
View File

@@ -0,0 +1,62 @@
# t00031 - PlantUML style decorator test case
## Config
```yaml
compilation_database_dir: ..
output_directory: puml
diagrams:
t00031_class:
type: class
glob:
- ../../tests/t00031/t00031.cc
using_namespace:
- clanguml::t00031
include:
namespaces:
- clanguml::t00031
```
## Source code
File t00031.cc
```cpp
#include <memory>
#include <vector>
namespace clanguml {
namespace t00031 {
/// @uml{style[#back:lightgreen|yellow;header:blue/red]}
class A {
};
/// @uml{style[#line.dotted:blue]}
enum B { one, two, three };
/// @uml{style[#pink;line:red;line.bold;text:red]}
template <typename T> class C {
T ttt;
};
class D {
};
struct R {
/// @uml{style[#red,dashed,thickness=2]}
A *aaa;
/// @uml{composition}
/// @uml{style[#green,dashed,thickness=4]}
std::vector<B> bbb;
/// @uml{style[#blue,dotted,thickness=8]}
C<int> ccc;
/// @uml{style[#blue,plain,thickness=16]}
D *ddd;
};
} // namespace t00031
} // namespace clanguml
```
## Generated UML diagrams
![t00031_class](./t00031_class.png "PlantUML style decorator test case")

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -75,24 +75,24 @@ public:
} }
} }
std::string to_string(relationship_t r) const std::string to_string(relationship_t r, std::string style = "") const
{ {
switch (r) { switch (r) {
case relationship_t::kOwnership: case relationship_t::kOwnership:
case relationship_t::kComposition: case relationship_t::kComposition:
return "*--"; return style.empty() ? "*--" : fmt::format("*-[{}]-", style);
case relationship_t::kAggregation: case relationship_t::kAggregation:
return "o--"; return style.empty() ? "o--" : fmt::format("o-[{}]-", style);
case relationship_t::kContainment: case relationship_t::kContainment:
return "--+"; return style.empty() ? "--+" : fmt::format("-[{}]-+", style);
case relationship_t::kAssociation: case relationship_t::kAssociation:
return "-->"; return style.empty() ? "-->" : fmt::format("-[{}]->", style);
case relationship_t::kInstantiation: case relationship_t::kInstantiation:
return "..|>"; return style.empty() ? "..|>" : fmt::format(".[{}].|>", style);
case relationship_t::kFriendship: case relationship_t::kFriendship:
return "<.."; return style.empty() ? "<.." : fmt::format("<.[{}].", style);
case relationship_t::kDependency: case relationship_t::kDependency:
return "..>"; return style.empty() ? "..>" : fmt::format(".[{}].>", style);
default: default:
return ""; return "";
} }
@@ -129,7 +129,7 @@ public:
ostr << class_type << " \"" << c.full_name(m_config.using_namespace); ostr << class_type << " \"" << c.full_name(m_config.using_namespace);
ostr << "\" as " << c.alias() << std::endl; ostr << "\" as " << c.alias() << '\n';
} }
void generate_alias(const enum_ &e, std::ostream &ostr) const void generate_alias(const enum_ &e, std::ostream &ostr) const
@@ -137,7 +137,7 @@ public:
ostr << "enum" ostr << "enum"
<< " \"" << e.full_name(m_config.using_namespace); << " \"" << e.full_name(m_config.using_namespace);
ostr << "\" as " << e.alias() << std::endl; ostr << "\" as " << e.alias() << '\n';
} }
void generate(const class_ &c, std::ostream &ostr) const void generate(const class_ &c, std::ostream &ostr) const
@@ -149,7 +149,12 @@ public:
if (c.is_abstract()) if (c.is_abstract())
class_type = "abstract"; class_type = "abstract";
ostr << class_type << " " << c.alias() << " {" << std::endl; ostr << class_type << " " << c.alias();
if (!c.style.empty())
ostr << " " << c.style;
ostr << " {" << '\n';
// //
// Process methods // Process methods
@@ -192,7 +197,7 @@ public:
ostr << " : " << ns_relative(uns, type); ostr << " : " << ns_relative(uns, type);
ostr << std::endl; ostr << '\n';
} }
// //
@@ -224,9 +229,18 @@ public:
destination = r.destination; destination = r.destination;
} }
std::string puml_relation;
if (!r.multiplicity_source.empty())
puml_relation += "\"" + r.multiplicity_source + "\" ";
puml_relation += to_string(r.type, r.style);
if (!r.multiplicity_destination.empty())
puml_relation += " \"" + r.multiplicity_destination + "\"";
relstr << m_model.to_alias( relstr << m_model.to_alias(
uns, ns_relative(uns, c.full_name(uns))) uns, ns_relative(uns, c.full_name(uns)))
<< " " << to_string(r.type) << " " << " " << puml_relation << " "
<< m_model.to_alias(uns, ns_relative(uns, destination)); << m_model.to_alias(uns, ns_relative(uns, destination));
if (!r.label.empty()) { if (!r.label.empty()) {
@@ -234,7 +248,7 @@ public:
rendered_relations.emplace(r.label); rendered_relations.emplace(r.label);
} }
relstr << std::endl; relstr << '\n';
all_relations_str << relstr.str(); all_relations_str << relstr.str();
} }
@@ -260,10 +274,10 @@ public:
ostr << "{static} "; ostr << "{static} ";
ostr << to_string(m.scope) << m.name << " : " ostr << to_string(m.scope) << m.name << " : "
<< ns_relative(uns, m.type) << std::endl; << ns_relative(uns, m.type) << '\n';
} }
ostr << "}" << std::endl; ostr << "}" << '\n';
if (m_config.should_include_relationship("inheritance")) if (m_config.should_include_relationship("inheritance"))
for (const auto &b : c.bases) { for (const auto &b : c.bases) {
@@ -273,7 +287,7 @@ public:
<< " <|-- " << " <|-- "
<< m_model.to_alias( << m_model.to_alias(
uns, ns_relative(uns, c.full_name(uns))) uns, ns_relative(uns, c.full_name(uns)))
<< std::endl; << '\n';
ostr << relstr.str(); ostr << relstr.str();
} }
catch (error::uml_alias_missing &e) { catch (error::uml_alias_missing &e) {
@@ -283,19 +297,36 @@ public:
} }
} }
//
// Process notes
//
for (auto decorator : c.decorators) {
auto note = std::dynamic_pointer_cast<decorators::note>(decorator);
if (note && note->applies_to_diagram(m_config.name)) {
ostr << "note " << note->position << " of " << c.alias() << '\n'
<< note->text << '\n'
<< "end note\n";
}
}
// Print relationships // Print relationships
ostr << all_relations_str.str(); ostr << all_relations_str.str();
} }
void generate(const enum_ &e, std::ostream &ostr) const void generate(const enum_ &e, std::ostream &ostr) const
{ {
ostr << "enum " << e.alias() << " {" << std::endl; ostr << "enum " << e.alias();
if (!e.style.empty())
ostr << " " << e.style;
ostr << " {" << '\n';
for (const auto &enum_constant : e.constants) { for (const auto &enum_constant : e.constants) {
ostr << enum_constant << std::endl; ostr << enum_constant << '\n';
} }
ostr << "}" << std::endl; ostr << "}" << '\n';
for (const auto &r : e.relationships) { for (const auto &r : e.relationships) {
if (!m_config.should_include_relationship(name(r.type))) if (!m_config.should_include_relationship(name(r.type)))
@@ -327,7 +358,8 @@ public:
if (!r.label.empty()) if (!r.label.empty())
relstr << " : " << r.label; relstr << " : " << r.label;
relstr << std::endl; relstr << '\n';
ostr << relstr.str(); ostr << relstr.str();
} }
catch (error::uml_alias_missing &ex) { catch (error::uml_alias_missing &ex) {
@@ -336,11 +368,23 @@ public:
to_string(r.type), e.name, destination, ex.what()); to_string(r.type), e.name, destination, ex.what());
} }
} }
//
// Process notes
//
for (auto decorator : e.decorators) {
auto note = std::dynamic_pointer_cast<decorators::note>(decorator);
if (note && note->applies_to_diagram(m_config.name)) {
ostr << "note " << note->position << " of " << e.alias() << '\n'
<< note->text << '\n'
<< "end note\n";
}
}
} }
void generate(std::ostream &ostr) const void generate(std::ostream &ostr) const
{ {
ostr << "@startuml" << std::endl; ostr << "@startuml" << '\n';
for (const auto &b : m_config.puml.before) { for (const auto &b : m_config.puml.before) {
std::string note{b}; std::string note{b};
@@ -352,7 +396,7 @@ public:
note.replace( note.replace(
std::get<1>(alias_match), std::get<2>(alias_match), alias); std::get<1>(alias_match), std::get<2>(alias_match), alias);
} }
ostr << note << std::endl; ostr << note << '\n';
} }
if (m_config.should_include_entities("classes")) { if (m_config.should_include_entities("classes")) {
@@ -361,14 +405,14 @@ public:
!m_config.should_include(c.name)) !m_config.should_include(c.name))
continue; continue;
generate_alias(c, ostr); generate_alias(c, ostr);
ostr << std::endl; ostr << '\n';
} }
for (const auto &e : m_model.enums) { for (const auto &e : m_model.enums) {
if (!m_config.should_include(e.name)) if (!m_config.should_include(e.name))
continue; continue;
generate_alias(e, ostr); generate_alias(e, ostr);
ostr << std::endl; ostr << '\n';
} }
for (const auto &c : m_model.classes) { for (const auto &c : m_model.classes) {
@@ -376,14 +420,14 @@ public:
!m_config.should_include(c.name)) !m_config.should_include(c.name))
continue; continue;
generate(c, ostr); generate(c, ostr);
ostr << std::endl; ostr << '\n';
} }
} }
if (m_config.should_include_entities("enums")) if (m_config.should_include_entities("enums"))
for (const auto &e : m_model.enums) { for (const auto &e : m_model.enums) {
generate(e, ostr); generate(e, ostr);
ostr << std::endl; ostr << '\n';
} }
// Process aliases in any of the puml directives // Process aliases in any of the puml directives
@@ -397,10 +441,10 @@ public:
note.replace( note.replace(
std::get<1>(alias_match), std::get<2>(alias_match), alias); std::get<1>(alias_match), std::get<2>(alias_match), alias);
} }
ostr << note << std::endl; ostr << note << '\n';
} }
ostr << "@enduml" << std::endl; ostr << "@enduml" << '\n';
} }
friend std::ostream &operator<<(std::ostream &os, const generator &g); friend std::ostream &operator<<(std::ostream &os, const generator &g);

View File

@@ -17,6 +17,7 @@
*/ */
#pragma once #pragma once
#include "decorators.h"
#include "util/error.h" #include "util/error.h"
#include "util/util.h" #include "util/util.h"
@@ -53,7 +54,61 @@ enum class relationship_t {
std::string to_string(relationship_t r); std::string to_string(relationship_t r);
class element { struct stylable_element {
std::string style;
};
struct decorated_element {
std::vector<std::shared_ptr<decorators::decorator>> decorators;
bool skip() const
{
for (auto d : decorators)
if (std::dynamic_pointer_cast<decorators::skip>(d))
return true;
return false;
}
bool skip_relationship() const
{
for (auto d : decorators)
if (std::dynamic_pointer_cast<decorators::skip_relationship>(d))
return true;
return false;
}
std::pair<relationship_t, std::string> relationship() const
{
for (auto &d : decorators)
if (std::dynamic_pointer_cast<decorators::association>(d))
return {relationship_t::kAssociation,
std::dynamic_pointer_cast<decorators::relationship>(d)
->multiplicity};
else if (std::dynamic_pointer_cast<decorators::aggregation>(d))
return {relationship_t::kAggregation,
std::dynamic_pointer_cast<decorators::relationship>(d)
->multiplicity};
else if (std::dynamic_pointer_cast<decorators::composition>(d))
return {relationship_t::kComposition,
std::dynamic_pointer_cast<decorators::relationship>(d)
->multiplicity};
return {relationship_t::kNone, ""};
}
std::string style_spec()
{
for (auto d : decorators)
if (std::dynamic_pointer_cast<decorators::style>(d))
return std::dynamic_pointer_cast<decorators::style>(d)->spec;
return "";
}
};
class element : public decorated_element {
public: public:
element() element()
: m_id{m_nextId++} : m_id{m_nextId++}
@@ -71,7 +126,7 @@ private:
static std::atomic_uint64_t m_nextId; static std::atomic_uint64_t m_nextId;
}; };
struct class_element { struct class_element : public decorated_element {
scope_t scope; scope_t scope;
std::string name; std::string name;
std::string type; std::string type;
@@ -82,7 +137,7 @@ struct class_member : public class_element {
bool is_static{false}; bool is_static{false};
}; };
struct method_parameter { struct method_parameter : public decorated_element {
std::string type; std::string type;
std::string name; std::string name;
std::string default_value; std::string default_value;
@@ -115,11 +170,11 @@ struct class_parent {
access_t access; access_t access;
}; };
struct class_relationship { struct class_relationship : public decorated_element, public stylable_element {
relationship_t type{relationship_t::kAssociation}; relationship_t type{relationship_t::kAssociation};
std::string destination; std::string destination;
std::string cardinality_source; std::string multiplicity_source;
std::string cardinality_destination; std::string multiplicity_destination;
std::string label; std::string label;
scope_t scope{scope_t::kNone}; scope_t scope{scope_t::kNone};
@@ -148,7 +203,7 @@ struct type_alias {
std::string underlying_type; std::string underlying_type;
}; };
class class_ : public element { class class_ : public element, public stylable_element {
public: public:
std::string usr; std::string usr;
bool is_struct{false}; bool is_struct{false};
@@ -233,7 +288,7 @@ public:
} }
}; };
struct enum_ : public element { struct enum_ : public element, public stylable_element {
std::vector<std::string> constants; std::vector<std::string> constants;
std::vector<class_relationship> relationships; std::vector<class_relationship> relationships;

View File

@@ -193,6 +193,18 @@ void tu_visitor::process_enum_declaration(const cppast::cpp_enum &enm)
enum_ e; enum_ e;
e.name = cx::util::full_name(ctx.namespace_, enm); e.name = cx::util::full_name(ctx.namespace_, enm);
if (enm.comment().has_value())
e.decorators = decorators::parse(enm.comment().value());
if (e.skip())
return;
e.style = e.style_spec();
// Process enum documentation comment
if (enm.comment().has_value())
e.decorators = decorators::parse(enm.comment().value());
for (const auto &ev : enm) { for (const auto &ev : enm) {
if (ev.kind() == cppast::cpp_entity_kind::enum_value_t) { if (ev.kind() == cppast::cpp_entity_kind::enum_value_t) {
e.constants.push_back(ev.name()); e.constants.push_back(ev.name());
@@ -226,9 +238,28 @@ void tu_visitor::process_class_declaration(const cppast::cpp_class &cls,
c.is_struct = cls.class_kind() == cppast::cpp_class_kind::struct_t; c.is_struct = cls.class_kind() == cppast::cpp_class_kind::struct_t;
c.name = cx::util::full_name(ctx.namespace_, cls); c.name = cx::util::full_name(ctx.namespace_, cls);
if (cls.comment().has_value())
c.decorators = decorators::parse(cls.comment().value());
cppast::cpp_access_specifier_kind last_access_specifier = cppast::cpp_access_specifier_kind last_access_specifier =
cppast::cpp_access_specifier_kind::cpp_private; cppast::cpp_access_specifier_kind::cpp_private;
// Process class documentation comment
if (cppast::is_templated(cls)) {
if (cls.parent().value().comment().has_value())
c.decorators =
decorators::parse(cls.parent().value().comment().value());
}
else {
if (cls.comment().has_value())
c.decorators = decorators::parse(cls.comment().value());
}
if (c.skip())
return;
c.style = c.style_spec();
// Process class child entities // Process class child entities
if (c.is_struct) if (c.is_struct)
last_access_specifier = cppast::cpp_access_specifier_kind::cpp_public; last_access_specifier = cppast::cpp_access_specifier_kind::cpp_public;
@@ -240,7 +271,6 @@ void tu_visitor::process_class_declaration(const cppast::cpp_class &cls,
} }
else if (child.kind() == cppast::cpp_entity_kind::member_variable_t) { else if (child.kind() == cppast::cpp_entity_kind::member_variable_t) {
auto &mv = static_cast<const cppast::cpp_member_variable &>(child); auto &mv = static_cast<const cppast::cpp_member_variable &>(child);
LOG_DBG("Found member variable {}", mv.name());
process_field(mv, c, last_access_specifier); process_field(mv, c, last_access_specifier);
} }
else if (child.kind() == cppast::cpp_entity_kind::variable_t) { else if (child.kind() == cppast::cpp_entity_kind::variable_t) {
@@ -489,7 +519,7 @@ void tu_visitor::process_class_declaration(const cppast::cpp_class &cls,
bool tu_visitor::process_field_with_template_instantiation( bool tu_visitor::process_field_with_template_instantiation(
const cppast::cpp_member_variable &mv, const cppast::cpp_type &tr, const cppast::cpp_member_variable &mv, const cppast::cpp_type &tr,
class_ &c, cppast::cpp_access_specifier_kind as) class_ &c, class_member &m, cppast::cpp_access_specifier_kind as)
{ {
LOG_DBG("Processing field with template instatiation type {}", LOG_DBG("Processing field with template instatiation type {}",
cppast::to_string(tr)); cppast::to_string(tr));
@@ -546,6 +576,17 @@ bool tu_visitor::process_field_with_template_instantiation(
rr.type = relationship_t::kAggregation; rr.type = relationship_t::kAggregation;
rr.label = mv.name(); rr.label = mv.name();
rr.scope = detail::cpp_access_specifier_to_scope(as); rr.scope = detail::cpp_access_specifier_to_scope(as);
rr.style = m.style_spec();
auto [decorator_rtype, decorator_rmult] = m.relationship();
if (decorator_rtype != relationship_t::kNone) {
rr.type = decorator_rtype;
auto mult = util::split(decorator_rmult, ":");
if (mult.size() == 2) {
rr.multiplicity_source = mult[0];
rr.multiplicity_destination = mult[1];
}
}
LOG_DBG("Adding field instantiation relationship {} {} {} : {}", LOG_DBG("Adding field instantiation relationship {} {} {} : {}",
rr.destination, model::class_diagram::to_string(rr.type), c.usr, rr.destination, model::class_diagram::to_string(rr.type), c.usr,
@@ -576,6 +617,12 @@ void tu_visitor::process_field(const cppast::cpp_member_variable &mv, class_ &c,
m.scope = detail::cpp_access_specifier_to_scope(as); m.scope = detail::cpp_access_specifier_to_scope(as);
m.is_static = false; m.is_static = false;
if (mv.comment().has_value())
m.decorators = decorators::parse(mv.comment().value());
if (m.skip())
return;
auto &tr = cx::util::unreferenced(cppast::remove_cv(mv.type())); auto &tr = cx::util::unreferenced(cppast::remove_cv(mv.type()));
LOG_DBG("Processing field {} with unreferenced type of kind {}", mv.name(), LOG_DBG("Processing field {} with unreferenced type of kind {}", mv.name(),
@@ -591,7 +638,7 @@ void tu_visitor::process_field(const cppast::cpp_member_variable &mv, class_ &c,
else if (tr.kind() == cppast::cpp_type_kind::template_instantiation_t) { else if (tr.kind() == cppast::cpp_type_kind::template_instantiation_t) {
template_instantiation_added_as_aggregation = template_instantiation_added_as_aggregation =
process_field_with_template_instantiation( process_field_with_template_instantiation(
mv, resolve_alias(tr), c, as); mv, resolve_alias(tr), c, m, as);
} }
else if (tr.kind() == cppast::cpp_type_kind::unexposed_t) { else if (tr.kind() == cppast::cpp_type_kind::unexposed_t) {
LOG_DBG( LOG_DBG(
@@ -599,7 +646,8 @@ void tu_visitor::process_field(const cppast::cpp_member_variable &mv, class_ &c,
// TODO // TODO
} }
if (!template_instantiation_added_as_aggregation && if (!m.skip_relationship() &&
!template_instantiation_added_as_aggregation &&
(tr.kind() != cppast::cpp_type_kind::builtin_t) && (tr.kind() != cppast::cpp_type_kind::builtin_t) &&
(tr.kind() != cppast::cpp_type_kind::template_parameter_t)) { (tr.kind() != cppast::cpp_type_kind::template_parameter_t)) {
const auto &ttt = resolve_alias(mv.type()); const auto &ttt = resolve_alias(mv.type());
@@ -613,6 +661,17 @@ void tu_visitor::process_field(const cppast::cpp_member_variable &mv, class_ &c,
r.type = relationship_type; r.type = relationship_type;
r.label = m.name; r.label = m.name;
r.scope = m.scope; r.scope = m.scope;
r.style = m.style_spec();
auto [decorator_rtype, decorator_rmult] = m.relationship();
if (decorator_rtype != relationship_t::kNone) {
r.type = decorator_rtype;
auto mult = util::split(decorator_rmult, ":");
if (mult.size() == 2) {
r.multiplicity_source = mult[0];
r.multiplicity_destination = mult[1];
}
}
LOG_DBG("Adding field relationship {} {} {} : {}", LOG_DBG("Adding field relationship {} {} {} : {}",
r.destination, model::class_diagram::to_string(r.type), r.destination, model::class_diagram::to_string(r.type),
@@ -650,6 +709,12 @@ void tu_visitor::process_static_field(const cppast::cpp_variable &mv, class_ &c,
m.scope = detail::cpp_access_specifier_to_scope(as); m.scope = detail::cpp_access_specifier_to_scope(as);
m.is_static = true; m.is_static = true;
if (mv.comment().has_value())
m.decorators = decorators::parse(mv.comment().value());
if (m.skip())
return;
c.members.emplace_back(std::move(m)); c.members.emplace_back(std::move(m));
} }
@@ -666,6 +731,12 @@ void tu_visitor::process_method(const cppast::cpp_member_function &mf,
m.is_static = false; m.is_static = false;
m.scope = detail::cpp_access_specifier_to_scope(as); m.scope = detail::cpp_access_specifier_to_scope(as);
if (mf.comment().has_value())
m.decorators = decorators::parse(mf.comment().value());
if (m.skip())
return;
for (auto &param : mf.parameters()) for (auto &param : mf.parameters())
process_function_parameter(param, m, c); process_function_parameter(param, m, c);
@@ -695,6 +766,12 @@ void tu_visitor::process_template_method(
m.is_static = false; m.is_static = false;
m.scope = detail::cpp_access_specifier_to_scope(as); m.scope = detail::cpp_access_specifier_to_scope(as);
if (mf.comment().has_value())
m.decorators = decorators::parse(mf.comment().value());
if (m.skip())
return;
for (auto &param : mf.function().parameters()) for (auto &param : mf.function().parameters())
process_function_parameter(param, m, c); process_function_parameter(param, m, c);
@@ -716,6 +793,12 @@ void tu_visitor::process_static_method(const cppast::cpp_function &mf,
m.is_static = true; m.is_static = true;
m.scope = detail::cpp_access_specifier_to_scope(as); m.scope = detail::cpp_access_specifier_to_scope(as);
if (mf.comment().has_value())
m.decorators = decorators::parse(mf.comment().value());
if (m.skip())
return;
for (auto &param : mf.parameters()) for (auto &param : mf.parameters())
process_function_parameter(param, m, c); process_function_parameter(param, m, c);
@@ -737,6 +820,12 @@ void tu_visitor::process_constructor(const cppast::cpp_constructor &mf,
m.is_static = false; m.is_static = false;
m.scope = detail::cpp_access_specifier_to_scope(as); m.scope = detail::cpp_access_specifier_to_scope(as);
if (mf.comment().has_value())
m.decorators = decorators::parse(mf.comment().value());
if (m.skip())
return;
for (auto &param : mf.parameters()) for (auto &param : mf.parameters())
process_function_parameter(param, m, c); process_function_parameter(param, m, c);
@@ -764,6 +853,13 @@ void tu_visitor::process_function_parameter(
{ {
method_parameter mp; method_parameter mp;
mp.name = param.name(); mp.name = param.name();
if (param.comment().has_value())
m.decorators = decorators::parse(param.comment().value());
if (mp.skip())
return;
const auto &param_type = const auto &param_type =
cppast::remove_cv(cx::util::unreferenced(param.type())); cppast::remove_cv(cx::util::unreferenced(param.type()));
if (param_type.kind() == cppast::cpp_type_kind::template_instantiation_t) { if (param_type.kind() == cppast::cpp_type_kind::template_instantiation_t) {
@@ -777,7 +873,7 @@ void tu_visitor::process_function_parameter(
} }
auto dv = param.default_value(); auto dv = param.default_value();
if (dv) if (dv) {
switch (dv.value().kind()) { switch (dv.value().kind()) {
case cppast::cpp_expression_kind::literal_t: case cppast::cpp_expression_kind::literal_t:
mp.default_value = mp.default_value =
@@ -794,20 +890,24 @@ void tu_visitor::process_function_parameter(
default: default:
mp.default_value = "{}"; mp.default_value = "{}";
} }
}
if (!mp.skip_relationship()) {
// find relationship for the type // find relationship for the type
std::vector<std::pair<std::string, relationship_t>> relationships; std::vector<std::pair<std::string, relationship_t>> relationships;
find_relationships(cppast::remove_cv(param.type()), relationships, find_relationships(cppast::remove_cv(param.type()), relationships,
relationship_t::kDependency); relationship_t::kDependency);
for (const auto &[type, relationship_type] : relationships) { for (const auto &[type, relationship_type] : relationships) {
if ((relationship_type != relationship_t::kNone) && (type != c.name)) { if ((relationship_type != relationship_t::kNone) &&
(type != c.name)) {
class_relationship r; class_relationship r;
r.destination = type; r.destination = type;
r.type = relationship_t::kDependency; r.type = relationship_t::kDependency;
r.label = ""; r.label = "";
LOG_DBG("Adding field relationship {} {} {} : {}", r.destination, LOG_DBG("Adding field relationship {} {} {} : {}",
model::class_diagram::to_string(r.type), c.usr, r.label); r.destination, model::class_diagram::to_string(r.type),
c.usr, r.label);
c.add_relationship(std::move(r)); c.add_relationship(std::move(r));
} }
@@ -828,15 +928,15 @@ void tu_visitor::process_function_parameter(
.get(ctx.entity_index)[0] .get(ctx.entity_index)[0]
.get()); .get());
LOG_DBG( LOG_DBG("Maybe building instantiation for: {}",
"Maybe building instantiation for: {}", primary_template_name); primary_template_name);
if (ctx.config.should_include(primary_template_name)) { if (ctx.config.should_include(primary_template_name)) {
class_ tinst = class_ tinst = build_template_instantiation(
build_template_instantiation(template_instantiation_type); template_instantiation_type);
LOG_DBG("Created template instantiation: {}, {}", tinst.name, LOG_DBG("Created template instantiation: {}, {}",
tinst.usr); tinst.name, tinst.usr);
class_relationship r; class_relationship r;
r.destination = tinst.base_template_usr; r.destination = tinst.base_template_usr;
@@ -848,15 +948,18 @@ void tu_visitor::process_function_parameter(
rr.destination = tinst.usr; rr.destination = tinst.usr;
rr.type = relationship_t::kDependency; rr.type = relationship_t::kDependency;
rr.label = ""; rr.label = "";
LOG_DBG("Adding field dependency relationship {} {} {} : {}", LOG_DBG(
rr.destination, model::class_diagram::to_string(rr.type), "Adding field dependency relationship {} {} {} : {}",
c.usr, rr.label); rr.destination,
model::class_diagram::to_string(rr.type), c.usr,
rr.label);
c.add_relationship(std::move(rr)); c.add_relationship(std::move(rr));
ctx.d.add_class(std::move(tinst)); ctx.d.add_class(std::move(tinst));
} }
} }
} }
}
m.parameters.emplace_back(std::move(mp)); m.parameters.emplace_back(std::move(mp));
} }
@@ -910,6 +1013,12 @@ void tu_visitor::process_friend(const cppast::cpp_friend &f, class_ &parent)
r.type = relationship_t::kFriendship; r.type = relationship_t::kFriendship;
r.label = "<<friend>>"; r.label = "<<friend>>";
if (f.comment().has_value())
r.decorators = decorators::parse(f.comment().value());
if (r.skip() || r.skip_relationship())
return;
if (f.type()) { if (f.type()) {
auto name = cppast::to_string(f.type().value()); auto name = cppast::to_string(f.type().value());

View File

@@ -166,6 +166,7 @@ public:
bool process_field_with_template_instantiation( bool process_field_with_template_instantiation(
const cppast::cpp_member_variable &mv, const cppast::cpp_type &tr, const cppast::cpp_member_variable &mv, const cppast::cpp_type &tr,
clanguml::model::class_diagram::class_ &c, clanguml::model::class_diagram::class_ &c,
clanguml::model::class_diagram::class_member &m,
cppast::cpp_access_specifier_kind as); cppast::cpp_access_specifier_kind as);
void process_static_field(const cppast::cpp_variable &mv, void process_static_field(const cppast::cpp_variable &mv,

219
src/uml/decorators.cc Normal file
View File

@@ -0,0 +1,219 @@
/**
* src/uml/decorators.cc
*
* Copyright (c) 2021 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 "decorators.h"
#include "util/util.h"
#include <string>
#include <string_view>
namespace clanguml {
namespace decorators {
const std::string note::label = "note";
const std::string skip::label = "skip";
const std::string skip_relationship::label = "skiprelationship";
const std::string style::label = "style";
const std::string aggregation::label = "aggregation";
const std::string composition::label = "composition";
const std::string association::label = "association";
std::shared_ptr<decorator> decorator::from_string(std::string_view c)
{
if (c.find(note::label) == 0) {
return note::from_string(c);
}
else if (c.find(skip_relationship::label) == 0) {
return skip_relationship::from_string(c);
}
else if (c.find(skip::label) == 0) {
return skip::from_string(c);
}
else if (c.find(style::label) == 0) {
return style::from_string(c);
}
else if (c.find(aggregation::label) == 0) {
return aggregation::from_string(c);
}
else if (c.find(composition::label) == 0) {
return composition::from_string(c);
}
else if (c.find(association::label) == 0) {
return association::from_string(c);
}
return {};
}
bool decorator::applies_to_diagram(std::string name)
{
return diagrams.empty() ||
(std::find(diagrams.begin(), diagrams.end(), name) != diagrams.end());
}
decorator_toks decorator::tokenize(const std::string &label, std::string_view c)
{
decorator_toks res;
res.label = label;
size_t pos{};
auto it = c.begin();
std::advance(it, label.size());
if (*it == ':') {
std::advance(it, 1);
pos = std::distance(c.begin(), it);
// If the diagram list is provided after ':', [] is mandatory
// even if empty
auto d = c.substr(pos, c.find("[", pos) - pos);
if (!d.empty()) {
std::string d_str{d};
d_str.erase(std::remove_if(d_str.begin(), d_str.end(),
(int (*)(int))std::isspace),
d_str.end());
res.diagrams = util::split(d_str, ",");
}
std::advance(it, d.size());
}
if (*it == '[') {
std::advance(it, 1);
pos = std::distance(c.begin(), it);
res.param = c.substr(pos, c.find("]", pos) - pos);
std::advance(it, res.param.size() + 1);
}
else if (std::isspace(*it)) {
std::advance(it, 1);
}
pos = std::distance(c.begin(), it);
res.text = c.substr(pos, c.find("}", pos) - pos);
res.text = util::trim(res.text);
res.param = util::trim(res.param);
return res;
}
std::shared_ptr<decorator> note::from_string(std::string_view c)
{
auto res = std::make_shared<note>();
auto toks = res->tokenize(note::label, c);
res->diagrams = toks.diagrams;
if (!toks.param.empty())
res->position = toks.param;
res->text = toks.text;
return res;
}
std::shared_ptr<decorator> skip::from_string(std::string_view c)
{
return std::make_shared<skip>();
}
std::shared_ptr<decorator> skip_relationship::from_string(std::string_view c)
{
return std::make_shared<skip_relationship>();
}
std::shared_ptr<decorator> style::from_string(std::string_view c)
{
auto res = std::make_shared<style>();
auto toks = res->tokenize(style::label, c);
res->diagrams = toks.diagrams;
res->spec = toks.param;
return res;
}
std::shared_ptr<decorator> aggregation::from_string(std::string_view c)
{
auto res = std::make_shared<aggregation>();
auto toks = res->tokenize(aggregation::label, c);
res->diagrams = toks.diagrams;
res->multiplicity = toks.param;
return res;
}
std::shared_ptr<decorator> composition::from_string(std::string_view c)
{
auto res = std::make_shared<composition>();
auto toks = res->tokenize(composition::label, c);
res->diagrams = toks.diagrams;
res->multiplicity = toks.param;
return res;
}
std::shared_ptr<decorator> association::from_string(std::string_view c)
{
auto res = std::make_shared<association>();
auto toks = res->tokenize(association::label, c);
res->diagrams = toks.diagrams;
res->multiplicity = toks.param;
return res;
}
std::vector<std::shared_ptr<decorator>> parse(
std::string documentation_block, std::string clanguml_tag)
{
std::vector<std::shared_ptr<decorator>> res;
const std::string begin_tag{"@" + clanguml_tag};
const auto begin_tag_size = begin_tag.size();
// First replace all \uml occurences with @uml
util::replace_all(
documentation_block, "\\" + clanguml_tag, "@" + clanguml_tag);
documentation_block = util::trim(documentation_block);
std::string_view block_view{documentation_block};
auto pos = block_view.find("@" + clanguml_tag + "{");
while (pos < documentation_block.size()) {
auto c_begin = pos + begin_tag_size;
auto c_end = documentation_block.find("}", c_begin);
if (c_end == std::string::npos)
return res;
auto com =
decorator::from_string(block_view.substr(c_begin + 1, c_end - 2));
if (com)
res.emplace_back(std::move(com));
pos = block_view.find("@" + clanguml_tag + "{", c_end);
}
return res;
};
} // namespace decorators
} // namespace clanguml

101
src/uml/decorators.h Normal file
View File

@@ -0,0 +1,101 @@
/**
* src/uml/decorators.h
*
* Copyright (c) 2021 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 <functional>
#include <map>
#include <memory>
#include <string>
namespace clanguml {
namespace decorators {
struct decorator_toks {
std::string label;
std::vector<std::string> diagrams;
std::string param;
std::string text;
};
struct decorator {
std::vector<std::string> diagrams;
virtual ~decorator() = default;
static std::shared_ptr<decorator> from_string(std::string_view c);
bool applies_to_diagram(std::string name);
protected:
decorator_toks tokenize(const std::string &label, std::string_view c);
};
struct note : public decorator {
static const std::string label;
std::string position{"left"};
std::string text;
static std::shared_ptr<decorator> from_string(std::string_view c);
};
struct skip : public decorator {
static const std::string label;
static std::shared_ptr<decorator> from_string(std::string_view c);
};
struct skip_relationship : public decorator {
static const std::string label;
static std::shared_ptr<decorator> from_string(std::string_view c);
};
struct style : public decorator {
static const std::string label;
std::string spec;
static std::shared_ptr<decorator> from_string(std::string_view c);
};
struct relationship : public decorator {
std::string multiplicity;
};
struct aggregation : public relationship {
static const std::string label;
static std::shared_ptr<decorator> from_string(std::string_view c);
};
struct composition : public relationship {
static const std::string label;
static std::shared_ptr<decorator> from_string(std::string_view c);
};
struct association : public relationship {
static const std::string label;
static std::shared_ptr<decorator> from_string(std::string_view c);
};
std::vector<std::shared_ptr<decorator>> parse(
std::string documentation_block, std::string clanguml_tag = "uml");
} // namespace decorators
} // namespace clanguml

View File

@@ -126,5 +126,20 @@ bool find_element_alias(
return true; return true;
} }
bool replace_all(
std::string &input, std::string pattern, std::string replace_with)
{
bool replaced{false};
auto pos = input.find(pattern);
while (pos < input.size()) {
input.replace(pos, pattern.size(), replace_with);
pos = input.find(pattern, pos + replace_with.size());
replaced = true;
}
return replaced;
}
} }
} }

View File

@@ -104,5 +104,15 @@ std::string unqualify(const std::string &s);
*/ */
bool find_element_alias( bool find_element_alias(
const std::string &input, std::tuple<std::string, size_t, size_t> &result); const std::string &input, std::tuple<std::string, size_t, size_t> &result);
/**
* @brief Find and replace in string
*
* Replaces all occurences of pattern with replace_with in input string.
*
* @return True if at least on replacement was made
*/
bool replace_all(
std::string &input, std::string pattern, std::string replace_with);
} }
} }

View File

@@ -23,6 +23,14 @@ set(CLANG_UML_TEST_CASES_HEADER
catch.h catch.h
) )
set(CLANG_UML_TEST_DECORATOR_PARSER_SRC
test_decorator_parser.cc
${TEST_UTIL_SOURCES}
)
set(CLANG_UML_TEST_DECORATOR_PARSER_HEADER
catch.h
)
add_executable(test_util add_executable(test_util
${CLANG_UML_TEST_UTIL_SRC} ${CLANG_UML_TEST_UTIL_SRC}
${CLANG_UML_TEST_UTIL_HEADER}) ${CLANG_UML_TEST_UTIL_HEADER})
@@ -33,6 +41,15 @@ target_link_libraries(test_util
${YAML_CPP_LIBRARIES} ${YAML_CPP_LIBRARIES}
spdlog::spdlog clang-umllib cppast) spdlog::spdlog clang-umllib cppast)
add_executable(test_decorator_parser
${CLANG_UML_TEST_DECORATOR_PARSER_SRC}
${CLANG_UML_TEST_DECORATOR_PARSER_HEADER})
target_link_libraries(test_decorator_parser
PRIVATE
${LIBCLANG_LIBRARIES}
${YAML_CPP_LIBRARIES}
spdlog::spdlog clang-umllib cppast)
add_executable(test_cases add_executable(test_cases
${CLANG_UML_TEST_CASES_SRC} ${CLANG_UML_TEST_CASES_SRC}
@@ -57,4 +74,5 @@ foreach(TEST_CASE_CONFIG ${TEST_CASE_CONFIGS})
endforeach() endforeach()
add_test(NAME test_util COMMAND test_util) add_test(NAME test_util COMMAND test_util)
add_test(NAME test_decorator_parser COMMAND test_decorator_parser)
add_test(NAME test_cases COMMAND test_cases) add_test(NAME test_cases COMMAND test_cases)

12
tests/t00028/.clang-uml Normal file
View File

@@ -0,0 +1,12 @@
compilation_database_dir: ..
output_directory: puml
diagrams:
t00028_class:
type: class
glob:
- ../../tests/t00028/t00028.cc
using_namespace:
- clanguml::t00028
include:
namespaces:
- clanguml::t00028

56
tests/t00028/t00028.cc Normal file
View File

@@ -0,0 +1,56 @@
#include <memory>
#include <vector>
namespace clanguml {
namespace t00028 {
/// \uml{note[top] A class note.}
class A {
};
/// \uml{note[] B class note.}
class B {
};
///
/// @uml{note:t00028_class[bottom] C class note.}
/// This is class C.
class C {
};
/// \uml{note
/// D
/// class
/// note.}
class D {
};
/// \uml{note E template class note.}
template <typename T> class E {
T param;
};
/// \uml{note:other_diagram[left] G class note.}
class G {
};
/// @uml{note[ bottom ] F enum note.}
enum class F { one, two, three };
/// \uml{note[right] R class note.}
class R {
A aaa;
B *bbb;
C &ccc;
std::vector<std::shared_ptr<D>> ddd;
E<int> eee;
G **ggg;
};
} // namespace t00028
} // namespace clanguml

67
tests/t00028/test_case.h Normal file
View File

@@ -0,0 +1,67 @@
/**
* tests/t00028/test_case.cc
*
* Copyright (c) 2021 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.
*/
TEST_CASE("t00028", "[test-case][class]")
{
auto [config, db] = load_config("t00028");
auto diagram = config.diagrams["t00028_class"];
REQUIRE(diagram->name == "t00028_class");
REQUIRE(diagram->include.namespaces.size() == 1);
REQUIRE_THAT(diagram->include.namespaces,
VectorContains(std::string{"clanguml::t00028"}));
REQUIRE(diagram->exclude.namespaces.size() == 0);
REQUIRE(diagram->should_include("clanguml::t00028::A"));
auto model = generate_class_diagram(db, diagram);
REQUIRE(model.name == "t00028_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, IsClass(_A("C")));
REQUIRE_THAT(puml, IsClass(_A("D")));
REQUIRE_THAT(puml, IsClassTemplate("E", "T"));
REQUIRE_THAT(puml, IsEnum(_A("F")));
REQUIRE_THAT(puml, IsClass(_A("R")));
REQUIRE_THAT(puml, HasNote(_A("A"), "top", "A class note."));
REQUIRE_THAT(puml, HasNote(_A("B"), "left", "B class note."));
REQUIRE_THAT(puml, HasNote(_A("C"), "bottom", "C class note."));
const auto d_note = R"(
D
class
note.)";
REQUIRE_THAT(puml, HasNote(_A("D"), "left", d_note));
REQUIRE_THAT(puml, HasNote(_A("E<T>"), "left", "E template class note."));
REQUIRE_THAT(puml, HasNote(_A("F"), "bottom", "F enum note."));
REQUIRE_THAT(puml, !HasNote(_A("G"), "left", "G class note."));
REQUIRE_THAT(puml, HasNote(_A("R"), "right", "R class note."));
save_puml(
"./" + config.output_directory + "/" + diagram->name + ".puml", puml);
}

12
tests/t00029/.clang-uml Normal file
View File

@@ -0,0 +1,12 @@
compilation_database_dir: ..
output_directory: puml
diagrams:
t00029_class:
type: class
glob:
- ../../tests/t00029/t00029.cc
using_namespace:
- clanguml::t00029
include:
namespaces:
- clanguml::t00029

53
tests/t00029/t00029.cc Normal file
View File

@@ -0,0 +1,53 @@
#include <memory>
#include <vector>
namespace clanguml {
namespace t00029 {
class A {
};
/// \uml{skip}
class B {
};
template <typename T> class C {
T param;
};
/// @uml{skip:t00029_class}
template <typename T> class D {
T param;
};
enum class E { one, two, three };
/// \uml{skip}
enum class F { red, green, blue };
class G1 {
};
class G2 {
};
class G3 {
};
class G4 {
};
struct R {
G1 g1;
/// \uml{skip}
G2 g2;
/// \uml{skiprelationship}
G3 &g3;
std::shared_ptr<G4> g4;
};
} // namespace t00029
} // namespace clanguml

65
tests/t00029/test_case.h Normal file
View File

@@ -0,0 +1,65 @@
/**
* tests/t00029/test_case.cc
*
* Copyright (c) 2021 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.
*/
TEST_CASE("t00029", "[test-case][class]")
{
auto [config, db] = load_config("t00029");
auto diagram = config.diagrams["t00029_class"];
REQUIRE(diagram->name == "t00029_class");
REQUIRE(diagram->include.namespaces.size() == 1);
REQUIRE_THAT(diagram->include.namespaces,
VectorContains(std::string{"clanguml::t00029"}));
REQUIRE(diagram->exclude.namespaces.size() == 0);
REQUIRE(diagram->should_include("clanguml::t00029::A"));
auto model = generate_class_diagram(db, diagram);
REQUIRE(model.name == "t00029_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, IsClassTemplate("C", "T"));
REQUIRE_THAT(puml, !IsClassTemplate("D", "T"));
REQUIRE_THAT(puml, IsEnum(_A("E")));
REQUIRE_THAT(puml, !IsEnum(_A("F")));
REQUIRE_THAT(puml, IsClass(_A("G1")));
REQUIRE_THAT(puml, IsClass(_A("G2")));
REQUIRE_THAT(puml, IsClass(_A("G3")));
REQUIRE_THAT(puml, IsClass(_A("G4")));
REQUIRE_THAT(puml, IsClass(_A("R")));
REQUIRE_THAT(puml, IsAggregation(_A("R"), _A("G1"), "+g1"));
REQUIRE_THAT(puml, !IsAggregation(_A("R"), _A("G2"), "+g2"));
REQUIRE_THAT(puml, !IsAggregation(_A("R"), _A("G3"), "+g3"));
REQUIRE_THAT(puml, IsAssociation(_A("R"), _A("G4"), "+g4"));
save_puml(
"./" + config.output_directory + "/" + diagram->name + ".puml", puml);
}

12
tests/t00030/.clang-uml Normal file
View File

@@ -0,0 +1,12 @@
compilation_database_dir: ..
output_directory: puml
diagrams:
t00030_class:
type: class
glob:
- ../../tests/t00030/t00030.cc
using_namespace:
- clanguml::t00030
include:
namespaces:
- clanguml::t00030

34
tests/t00030/t00030.cc Normal file
View File

@@ -0,0 +1,34 @@
#include <memory>
#include <vector>
namespace clanguml {
namespace t00030 {
class A {
};
class B {
};
class C {
};
class D {
};
struct R {
/// @uml{association[]}
A aaa;
/// @uml{composition[0..1:1..*]}
std::vector<B> bbb;
/// @uml{aggregation[0..1:1..5]}
std::vector<C> ccc;
/// @uml{association[:1]}
D ddd;
};
} // namespace t00030
} // namespace clanguml

57
tests/t00030/test_case.h Normal file
View File

@@ -0,0 +1,57 @@
/**
* tests/t00030/test_case.cc
*
* Copyright (c) 2021 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.
*/
TEST_CASE("t00030", "[test-case][class]")
{
auto [config, db] = load_config("t00030");
auto diagram = config.diagrams["t00030_class"];
REQUIRE(diagram->name == "t00030_class");
REQUIRE(diagram->include.namespaces.size() == 1);
REQUIRE_THAT(diagram->include.namespaces,
VectorContains(std::string{"clanguml::t00030"}));
REQUIRE(diagram->exclude.namespaces.size() == 0);
REQUIRE(diagram->should_include("clanguml::t00030::A"));
auto model = generate_class_diagram(db, diagram);
REQUIRE(model.name == "t00030_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, IsClass(_A("C")));
REQUIRE_THAT(puml, IsClass(_A("D")));
REQUIRE_THAT(puml, IsAssociation(_A("R"), _A("A"), "+aaa"));
REQUIRE_THAT(puml, IsComposition(_A("R"), _A("B"), "+bbb", "0..1", "1..*"));
REQUIRE_THAT(puml, IsAggregation(_A("R"), _A("C"), "+ccc", "0..1", "1..5"));
REQUIRE_THAT(puml, IsAssociation(_A("R"), _A("D"), "+ddd", "", "1"));
save_puml(
"./" + config.output_directory + "/" + diagram->name + ".puml", puml);
}

12
tests/t00031/.clang-uml Normal file
View File

@@ -0,0 +1,12 @@
compilation_database_dir: ..
output_directory: puml
diagrams:
t00031_class:
type: class
glob:
- ../../tests/t00031/t00031.cc
using_namespace:
- clanguml::t00031
include:
namespaces:
- clanguml::t00031

38
tests/t00031/t00031.cc Normal file
View File

@@ -0,0 +1,38 @@
#include <memory>
#include <vector>
namespace clanguml {
namespace t00031 {
/// @uml{style[#back:lightgreen|yellow;header:blue/red]}
class A {
};
/// @uml{style[#line.dotted:blue]}
enum B { one, two, three };
/// @uml{style[#pink;line:red;line.bold;text:red]}
template <typename T> class C {
T ttt;
};
class D {
};
struct R {
/// @uml{style[#red,dashed,thickness=2]}
A *aaa;
/// @uml{composition}
/// @uml{style[#green,dashed,thickness=4]}
std::vector<B> bbb;
/// @uml{style[#blue,dotted,thickness=8]}
C<int> ccc;
/// @uml{style[#blue,plain,thickness=16]}
D *ddd;
};
} // namespace t00031
} // namespace clanguml

65
tests/t00031/test_case.h Normal file
View File

@@ -0,0 +1,65 @@
/**
* tests/t00031/test_case.cc
*
* Copyright (c) 2021 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.
*/
TEST_CASE("t00031", "[test-case][class]")
{
auto [config, db] = load_config("t00031");
auto diagram = config.diagrams["t00031_class"];
REQUIRE(diagram->name == "t00031_class");
REQUIRE(diagram->include.namespaces.size() == 1);
REQUIRE_THAT(diagram->include.namespaces,
VectorContains(std::string{"clanguml::t00031"}));
REQUIRE(diagram->exclude.namespaces.size() == 0);
REQUIRE(diagram->should_include("clanguml::t00031::A"));
auto model = generate_class_diagram(db, diagram);
REQUIRE(model.name == "t00031_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, IsEnum(_A("B")));
REQUIRE_THAT(puml, IsClassTemplate("C", "T"));
REQUIRE_THAT(puml, IsClass(_A("D")));
REQUIRE_THAT(puml,
IsAssociationWithStyle(
_A("R"), _A("A"), "+aaa", "#red,dashed,thickness=2"));
REQUIRE_THAT(puml,
IsCompositionWithStyle(
_A("R"), _A("B"), "+bbb", "#green,dashed,thickness=4"));
REQUIRE_THAT(puml,
IsAggregationWithStyle(
_A("R"), _A("C<int>"), "+ccc", "#blue,dotted,thickness=8"));
REQUIRE_THAT(puml,
IsAssociationWithStyle(
_A("R"), _A("D"), "+ddd", "#blue,plain,thickness=16"));
save_puml(
"./" + config.output_directory + "/" + diagram->name + ".puml", puml);
}

View File

@@ -131,6 +131,10 @@ using namespace clanguml::test::matchers;
#include "t00025/test_case.h" #include "t00025/test_case.h"
#include "t00026/test_case.h" #include "t00026/test_case.h"
#include "t00027/test_case.h" #include "t00027/test_case.h"
#include "t00028/test_case.h"
#include "t00029/test_case.h"
#include "t00030/test_case.h"
#include "t00031/test_case.h"
// //
// Sequence diagram tests // Sequence diagram tests

View File

@@ -170,8 +170,7 @@ struct AliasMatcher {
} }
} }
throw std::runtime_error(fmt::format( return "__INVALID__ALIAS__";
"Cannot find alias {} in {}", name, fmt::join(puml, "\n")));
} }
const std::vector<std::string> puml; const std::vector<std::string> puml;
@@ -218,11 +217,23 @@ ContainsMatcher IsInnerClass(std::string const &parent,
} }
ContainsMatcher IsAssociation(std::string const &from, std::string const &to, ContainsMatcher IsAssociation(std::string const &from, std::string const &to,
std::string const &label, std::string const &label, std::string multiplicity_source = "",
std::string multiplicity_dest = "",
CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes) CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes)
{ {
std::string format_string = "{}";
if (!multiplicity_source.empty())
format_string += " \"" + multiplicity_source + "\"";
format_string += " -->";
if (!multiplicity_dest.empty())
format_string += " \"" + multiplicity_dest + "\"";
format_string += " {} : {}";
return ContainsMatcher(CasedString( return ContainsMatcher(CasedString(
fmt::format("{} --> {} : {}", from, to, label), caseSensitivity)); fmt::format(format_string, from, to, label), caseSensitivity));
} }
ContainsMatcher IsFriend(std::string const &from, std::string const &to, ContainsMatcher IsFriend(std::string const &from, std::string const &to,
@@ -233,19 +244,70 @@ ContainsMatcher IsFriend(std::string const &from, std::string const &to,
} }
ContainsMatcher IsComposition(std::string const &from, std::string const &to, ContainsMatcher IsComposition(std::string const &from, std::string const &to,
std::string const &label, std::string const &label, std::string multiplicity_source = "",
std::string multiplicity_dest = "",
CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes) CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes)
{ {
std::string format_string = "{}";
if (!multiplicity_source.empty())
format_string += " \"" + multiplicity_source + "\"";
format_string += " *--";
if (!multiplicity_dest.empty())
format_string += " \"" + multiplicity_dest + "\"";
format_string += " {} : {}";
return ContainsMatcher(CasedString( return ContainsMatcher(CasedString(
fmt::format("{} *-- {} : {}", from, to, label), caseSensitivity)); fmt::format(format_string, from, to, label), caseSensitivity));
} }
ContainsMatcher IsAggregation(std::string const &from, std::string const &to, ContainsMatcher IsAggregation(std::string const &from, std::string const &to,
std::string const &label, std::string const &label, std::string multiplicity_source = "",
std::string multiplicity_dest = "",
CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes) CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes)
{ {
std::string format_string = "{}";
if (!multiplicity_source.empty())
format_string += " \"" + multiplicity_source + "\"";
format_string += " o--";
if (!multiplicity_dest.empty())
format_string += " \"" + multiplicity_dest + "\"";
format_string += " {} : {}";
return ContainsMatcher(CasedString( return ContainsMatcher(CasedString(
fmt::format("{} o-- {} : {}", from, to, label), caseSensitivity)); fmt::format(format_string, from, to, label), caseSensitivity));
}
ContainsMatcher IsAggregationWithStyle(std::string const &from,
std::string const &to, std::string const &label, std::string style,
CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes)
{
return ContainsMatcher(
CasedString(fmt::format("{} o-[{}]- {} : {}", from, style, to, label),
caseSensitivity));
}
ContainsMatcher IsAssociationWithStyle(std::string const &from,
std::string const &to, std::string const &label, std::string style,
CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes)
{
return ContainsMatcher(
CasedString(fmt::format("{} -[{}]-> {} : {}", from, style, to, label),
caseSensitivity));
}
ContainsMatcher IsCompositionWithStyle(std::string const &from,
std::string const &to, std::string const &label, std::string style,
CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes)
{
return ContainsMatcher(
CasedString(fmt::format("{} *-[{}]- {} : {}", from, style, to, label),
caseSensitivity));
} }
ContainsMatcher IsInstantiation(std::string const &from, std::string const &to, ContainsMatcher IsInstantiation(std::string const &from, std::string const &to,
@@ -262,6 +324,14 @@ ContainsMatcher IsDependency(std::string const &from, std::string const &to,
CasedString(fmt::format("{} ..> {}", from, to), caseSensitivity)); CasedString(fmt::format("{} ..> {}", from, to), caseSensitivity));
} }
ContainsMatcher HasNote(std::string const &cls, std::string const &position,
std::string const &note,
CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes)
{
return ContainsMatcher(CasedString(
fmt::format("note {} of {}", position, cls), caseSensitivity));
}
template <typename... Ts> template <typename... Ts>
ContainsMatcher IsMethod(std::string const &name, ContainsMatcher IsMethod(std::string const &name,
std::string const &type = "void", std::string const &type = "void",

View File

@@ -78,6 +78,18 @@ test_cases:
- name: t00027 - name: t00027
title: Template decorator pattern title: Template decorator pattern
description: description:
- name: t00028
title: PlantUML note decorator test case
description:
- name: t00029
title: PlantUML skip decorator test case
description:
- name: t00030
title: PlantUML relationship decorators test case
description:
- name: t00031
title: PlantUML style decorator test case
description:
Sequence diagrams: Sequence diagrams:
- name: t20001 - name: t20001
title: Basic sequence diagram title: Basic sequence diagram

View File

@@ -0,0 +1,235 @@
/**
* tests/test_decorator_parser.cc
*
* Copyright (c) 2021 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.
*/
#define CATCH_CONFIG_MAIN
#include "uml/decorators.h"
#include "catch.h"
TEST_CASE("Test decorator parser on regular comment", "[unit-test]")
{
std::string comment = R"(
\brief This is a comment.
This is a longer comment.
\param a int an int
\param b float a float
)";
using namespace clanguml::decorators;
auto decorators = parse(comment);
CHECK(decorators.empty());
}
TEST_CASE("Test decorator parser on note", "[unit-test]")
{
std::string comment = R"(
\brief This is a comment.
This is a longer comment.
\
\param a int an int
\param b float a float
\uml{note[left] This is a comment}
@uml{note[ top ]
This is a comment }
\uml{note This is a comment}
\uml{note[] This is a comment}
)";
using namespace clanguml::decorators;
auto decorators = parse(comment);
CHECK(decorators.size() == 4);
auto n1 = std::dynamic_pointer_cast<note>(decorators.at(0));
auto n2 = std::dynamic_pointer_cast<note>(decorators.at(1));
auto n3 = std::dynamic_pointer_cast<note>(decorators.at(2));
auto n4 = std::dynamic_pointer_cast<note>(decorators.at(3));
CHECK(n1);
CHECK(n1->position == "left");
CHECK(n1->text == "This is a comment");
CHECK(n2);
CHECK(n2->position == "top");
CHECK(n2->text == "This is a comment");
CHECK(n3);
CHECK(n3->position == "left");
CHECK(n3->text == "This is a comment");
CHECK(n4);
CHECK(n4->position == "left");
CHECK(n4->text == "This is a comment");
}
TEST_CASE("Test decorator parser on note with custom tag", "[unit-test]")
{
std::string comment = R"(
\brief This is a comment.
This is a longer comment.
\
\param a int an int
\param b float a float
\clanguml{note[left] This is a comment}
@clanguml{note[ top ]
This is a comment }
\clanguml{note This is a comment}
\clanguml{note[] This is a comment}
)";
using namespace clanguml::decorators;
auto decorators = parse(comment, "clanguml");
CHECK(decorators.size() == 4);
auto n1 = std::dynamic_pointer_cast<note>(decorators.at(0));
auto n2 = std::dynamic_pointer_cast<note>(decorators.at(1));
auto n3 = std::dynamic_pointer_cast<note>(decorators.at(2));
auto n4 = std::dynamic_pointer_cast<note>(decorators.at(3));
CHECK(n1);
CHECK(n1->position == "left");
CHECK(n1->text == "This is a comment");
CHECK(n2);
CHECK(n2->position == "top");
CHECK(n2->text == "This is a comment");
CHECK(n3);
CHECK(n3->position == "left");
CHECK(n3->text == "This is a comment");
CHECK(n4);
CHECK(n4->position == "left");
CHECK(n4->text == "This is a comment");
}
TEST_CASE("Test decorator parser on style", "[unit-test]")
{
std::string comment = R"(
\uml{style[#green,dashed,thickness=4]}
)";
using namespace clanguml::decorators;
auto decorators = parse(comment);
CHECK(decorators.size() == 1);
auto n1 = std::dynamic_pointer_cast<style>(decorators.at(0));
CHECK(n1);
CHECK(n1->spec == "#green,dashed,thickness=4");
}
TEST_CASE("Test decorator parser on aggregation", "[unit-test]")
{
std::string comment = R"(
\uml{aggregation[0..1:0..*]}
)";
using namespace clanguml::decorators;
auto decorators = parse(comment);
CHECK(decorators.size() == 1);
auto n1 = std::dynamic_pointer_cast<aggregation>(decorators.at(0));
CHECK(n1);
CHECK(n1->multiplicity == "0..1:0..*");
}
TEST_CASE("Test decorator parser on skip", "[unit-test]")
{
std::string comment = R"(
\uml{skip}
)";
using namespace clanguml::decorators;
auto decorators = parse(comment);
CHECK(decorators.size() == 1);
auto n1 = std::dynamic_pointer_cast<skip>(decorators.at(0));
CHECK(n1);
}
TEST_CASE("Test decorator parser on skiprelationship", "[unit-test]")
{
std::string comment = R"(
\uml{skiprelationship}
)";
using namespace clanguml::decorators;
auto decorators = parse(comment);
CHECK(decorators.size() == 1);
auto n1 = std::dynamic_pointer_cast<skip_relationship>(decorators.at(0));
CHECK(n1);
}
TEST_CASE("Test decorator parser on diagram scope", "[unit-test]")
{
std::string comment = R"(
\uml{note:diagram1, diagram2,
diagram3[left] Note only for diagrams 1, 2 and 3.}
)";
using namespace clanguml::decorators;
auto decorators = parse(comment);
CHECK(decorators.size() == 1);
auto n1 = std::dynamic_pointer_cast<note>(decorators.at(0));
CHECK(n1);
CHECK(n1->diagrams.size() == 3);
CHECK(n1->diagrams[0] == "diagram1");
CHECK(n1->diagrams[1] == "diagram2");
CHECK(n1->diagrams[2] == "diagram3");
CHECK(n1->position == "left");
CHECK(n1->text == "Note only for diagrams 1, 2 and 3.");
CHECK(n1->applies_to_diagram("diagram2"));
CHECK(!n1->applies_to_diagram("diagram4"));
}

View File

@@ -46,3 +46,25 @@ TEST_CASE("Test ns_relative", "[unit-test]")
"static const vector<a>&"); "static const vector<a>&");
CHECK(ns_relative({"clanguml::t0"}, "clanguml::t0") == "t0"); CHECK(ns_relative({"clanguml::t0"}, "clanguml::t0") == "t0");
} }
TEST_CASE("Test replace_all", "[unit-test]")
{
using namespace clanguml::util;
const std::string orig = R"(
Lorem ipsum \clanguml{note} something something...
\clanguml{style}
)";
std::string text{orig};
CHECK(replace_all(text, "NOTHERE", "HERE") == false);
CHECK(replace_all(text, "\\clanguml", "@clanguml") == true);
CHECK(replace_all(text, "something", "nothing") == true);
CHECK(replace_all(text, "something", "nothing") == false);
CHECK(replace_all(text, "nothing", "something") == true);
CHECK(replace_all(text, "@clanguml", "\\clanguml") == true);
CHECK(text == orig);
}

View File

@@ -46,7 +46,7 @@ with open(r'tests/test_cases.yaml') as f:
tc.write("{}\n".format(test_case['description'])) tc.write("{}\n".format(test_case['description']))
# Write test config file # Write test config file
config = open('tests/{0}/.clanguml'.format(name), 'r').read() config = open('tests/{0}/.clang-uml'.format(name), 'r').read()
tc.write("## Config\n") tc.write("## Config\n")
tc.write("```yaml\n") tc.write("```yaml\n")
tc.write(config) tc.write(config)