Added support for 'class' diagrams from C99/C11 translation units (#97)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
# CHANGELOG
|
||||
|
||||
* Added support for plain C11 translation units (#97)
|
||||
* Added 'row' and 'column' layout hints for aligning elements (#90)
|
||||
* Added 'together' layout hint for grouping elements (#43)
|
||||
* Enabled adding notes to class methods and members (#87)
|
||||
|
||||
@@ -28,6 +28,7 @@ Main features supported so far include:
|
||||
* Diagram content filtering based on namespaces, elements and relationships
|
||||
* Optional package generation from namespaces
|
||||
* Interactive links to online code to classes, methods and class fields in SVG diagrams
|
||||
* Support for plain C99/C11 code (struct and units relationships)
|
||||
* **Sequence diagram generation**
|
||||
* Generation of sequence diagram from specific method or function
|
||||
* Generation of loop and conditional statements
|
||||
|
||||
@@ -116,6 +116,10 @@ void generator::generate(const class_ &c, std::ostream &ostr) const
|
||||
|
||||
ostr << class_type << " " << c.alias();
|
||||
|
||||
if (c.is_union())
|
||||
ostr << " "
|
||||
<< "<<union>>";
|
||||
|
||||
if (m_config.generate_links) {
|
||||
common_generator<diagram_config, diagram_model>::generate_link(ostr, c);
|
||||
}
|
||||
|
||||
@@ -54,6 +54,12 @@ public:
|
||||
bool is_template_instantiation() const;
|
||||
void is_template_instantiation(bool is_template_instantiation);
|
||||
|
||||
bool is_alias() const { return is_alias_; }
|
||||
void is_alias(bool alias) { is_alias_ = alias; }
|
||||
|
||||
bool is_union() const { return is_union_; }
|
||||
void is_union(bool u) { is_union_ = u; }
|
||||
|
||||
void add_member(class_member &&member);
|
||||
void add_method(class_method &&method);
|
||||
void add_parent(class_parent &&parent);
|
||||
@@ -73,10 +79,6 @@ public:
|
||||
|
||||
bool is_abstract() const;
|
||||
|
||||
bool is_alias() const { return is_alias_; }
|
||||
|
||||
void is_alias(bool alias) { is_alias_ = alias; }
|
||||
|
||||
void find_relationships(
|
||||
std::vector<std::pair<std::string, common::model::relationship_t>>
|
||||
&nested_relationships);
|
||||
@@ -89,6 +91,7 @@ private:
|
||||
bool is_template_{false};
|
||||
bool is_template_instantiation_{false};
|
||||
bool is_alias_{false};
|
||||
bool is_union_{false};
|
||||
std::vector<class_member> members_;
|
||||
std::vector<class_method> methods_;
|
||||
std::vector<class_parent> bases_;
|
||||
|
||||
@@ -310,6 +310,63 @@ bool translation_unit_visitor::VisitClassTemplateDecl(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool translation_unit_visitor::VisitRecordDecl(clang::RecordDecl *rec)
|
||||
{
|
||||
// Skip system headers
|
||||
if (source_manager().isInSystemHeader(rec->getSourceRange().getBegin()))
|
||||
return true;
|
||||
|
||||
if (clang::dyn_cast_or_null<clang::CXXRecordDecl>(rec))
|
||||
// This is handled by VisitCXXRecordDecl()
|
||||
return true;
|
||||
|
||||
// It seems we are in a C (not C++) translation unit
|
||||
if (!diagram().should_include(rec->getQualifiedNameAsString()))
|
||||
return true;
|
||||
|
||||
LOG_DBG("= Visiting record declaration {} at {}",
|
||||
rec->getQualifiedNameAsString(),
|
||||
rec->getLocation().printToString(source_manager()));
|
||||
|
||||
auto record_ptr = create_record_declaration(rec);
|
||||
|
||||
if (!record_ptr)
|
||||
return true;
|
||||
|
||||
const auto rec_id = record_ptr->id();
|
||||
|
||||
set_ast_local_id(rec->getID(), rec_id);
|
||||
|
||||
auto &record_model = diagram().get_class(rec_id).has_value()
|
||||
? *diagram().get_class(rec_id).get()
|
||||
: *record_ptr;
|
||||
|
||||
if (rec->isCompleteDefinition() && !record_model.complete()) {
|
||||
process_record_members(rec, record_model);
|
||||
record_model.complete(true);
|
||||
}
|
||||
|
||||
auto id = record_model.id();
|
||||
if (!rec->isCompleteDefinition()) {
|
||||
forward_declarations_.emplace(id, std::move(record_ptr));
|
||||
return true;
|
||||
}
|
||||
forward_declarations_.erase(id);
|
||||
|
||||
if (diagram_.should_include(record_model)) {
|
||||
LOG_DBG("Adding struct/union {} with id {}",
|
||||
record_model.full_name(false), record_model.id());
|
||||
|
||||
diagram_.add_class(std::move(record_ptr));
|
||||
}
|
||||
else {
|
||||
LOG_DBG("Skipping struct/union {} with id {}", record_model.full_name(),
|
||||
record_model.id());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool translation_unit_visitor::VisitCXXRecordDecl(clang::CXXRecordDecl *cls)
|
||||
{
|
||||
// Skip system headers
|
||||
@@ -383,6 +440,42 @@ bool translation_unit_visitor::VisitCXXRecordDecl(clang::CXXRecordDecl *cls)
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<class_> translation_unit_visitor::create_record_declaration(
|
||||
clang::RecordDecl *rec)
|
||||
{
|
||||
assert(rec != nullptr);
|
||||
|
||||
auto record_ptr{std::make_unique<class_>(config_.using_namespace())};
|
||||
auto &record = *record_ptr;
|
||||
|
||||
auto qualified_name = rec->getQualifiedNameAsString();
|
||||
|
||||
if (!diagram().should_include(qualified_name))
|
||||
return {};
|
||||
|
||||
process_record_parent(rec, record, namespace_{});
|
||||
|
||||
if (!record.is_nested()) {
|
||||
record.set_name(common::get_tag_name(*rec));
|
||||
record.set_id(common::to_id(record.full_name(false)));
|
||||
}
|
||||
|
||||
process_comment(*rec, record);
|
||||
set_source_location(*rec, record);
|
||||
|
||||
const auto record_full_name = record_ptr->full_name(false);
|
||||
|
||||
record.is_struct(rec->isStruct());
|
||||
record.is_union(rec->isUnion());
|
||||
|
||||
if (record.skip())
|
||||
return {};
|
||||
|
||||
record.set_style(record.style_spec());
|
||||
|
||||
return record_ptr;
|
||||
}
|
||||
|
||||
std::unique_ptr<class_> translation_unit_visitor::create_class_declaration(
|
||||
clang::CXXRecordDecl *cls)
|
||||
{
|
||||
@@ -392,14 +485,37 @@ std::unique_ptr<class_> translation_unit_visitor::create_class_declaration(
|
||||
auto &c = *c_ptr;
|
||||
|
||||
// TODO: refactor to method get_qualified_name()
|
||||
auto qualified_name =
|
||||
cls->getQualifiedNameAsString(); // common::get_qualified_name(*cls);
|
||||
auto qualified_name = cls->getQualifiedNameAsString();
|
||||
|
||||
if (!diagram().should_include(qualified_name))
|
||||
return {};
|
||||
|
||||
auto ns = common::get_tag_namespace(*cls);
|
||||
|
||||
process_record_parent(cls, c, ns);
|
||||
|
||||
if (!c.is_nested()) {
|
||||
c.set_name(common::get_tag_name(*cls));
|
||||
c.set_namespace(ns);
|
||||
c.set_id(common::to_id(c.full_name(false)));
|
||||
}
|
||||
|
||||
c.is_struct(cls->isStruct());
|
||||
|
||||
process_comment(*cls, c);
|
||||
set_source_location(*cls, c);
|
||||
|
||||
if (c.skip())
|
||||
return {};
|
||||
|
||||
c.set_style(c.style_spec());
|
||||
|
||||
return c_ptr;
|
||||
}
|
||||
|
||||
void translation_unit_visitor::process_record_parent(
|
||||
clang::RecordDecl *cls, class_ &c, const namespace_ &ns)
|
||||
{
|
||||
const auto *parent = cls->getParent();
|
||||
|
||||
std::optional<common::model::diagram_element::id_t> id_opt;
|
||||
@@ -435,7 +551,8 @@ std::unique_ptr<class_> translation_unit_visitor::create_class_declaration(
|
||||
assert(parent_class);
|
||||
|
||||
c.set_namespace(ns);
|
||||
if (cls->getNameAsString().empty()) {
|
||||
const auto cls_name = cls->getNameAsString();
|
||||
if (cls_name.empty()) {
|
||||
// Nested structs can be anonymous
|
||||
if (anonymous_struct_relationships_.count(cls->getID()) > 0) {
|
||||
const auto &[label, hint, access] =
|
||||
@@ -467,23 +584,6 @@ std::unique_ptr<class_> translation_unit_visitor::create_class_declaration(
|
||||
|
||||
c.nested(true);
|
||||
}
|
||||
else {
|
||||
c.set_name(common::get_tag_name(*cls));
|
||||
c.set_namespace(ns);
|
||||
c.set_id(common::to_id(c.full_name(false)));
|
||||
}
|
||||
|
||||
c.is_struct(cls->isStruct());
|
||||
|
||||
process_comment(*cls, c);
|
||||
set_source_location(*cls, c);
|
||||
|
||||
if (c.skip())
|
||||
return {};
|
||||
|
||||
c.set_style(c.style_spec());
|
||||
|
||||
return c_ptr;
|
||||
}
|
||||
|
||||
void translation_unit_visitor::process_class_declaration(
|
||||
@@ -715,6 +815,16 @@ void translation_unit_visitor::process_template_specialization_children(
|
||||
}
|
||||
}
|
||||
|
||||
void translation_unit_visitor::process_record_members(
|
||||
const clang::RecordDecl *cls, class_ &c)
|
||||
{
|
||||
// Iterate over regular class fields
|
||||
for (const auto *field : cls->fields()) {
|
||||
if (field != nullptr)
|
||||
process_field(*field, c);
|
||||
}
|
||||
}
|
||||
|
||||
void translation_unit_visitor::process_class_children(
|
||||
const clang::CXXRecordDecl *cls, class_ &c)
|
||||
{
|
||||
@@ -1054,11 +1164,16 @@ bool translation_unit_visitor::find_relationships(const clang::QualType &type,
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
else if (type->getAsCXXRecordDecl()) {
|
||||
const auto target_id = common::to_id(*type->getAsCXXRecordDecl());
|
||||
relationships.emplace_back(target_id, relationship_hint);
|
||||
result = true;
|
||||
}
|
||||
else {
|
||||
const auto target_id = common::to_id(*type->getAsRecordDecl());
|
||||
relationships.emplace_back(target_id, relationship_hint);
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -2092,6 +2207,9 @@ bool translation_unit_visitor::build_template_instantiation_add_base_classes(
|
||||
void translation_unit_visitor::process_field(
|
||||
const clang::FieldDecl &field_declaration, class_ &c)
|
||||
{
|
||||
LOG_DBG(
|
||||
"== Visiting record member {}", field_declaration.getNameAsString());
|
||||
|
||||
// Default hint for relationship is aggregation
|
||||
auto relationship_hint = relationship_t::kAggregation;
|
||||
// If the first type of the template instantiation of this field type
|
||||
@@ -2108,6 +2226,7 @@ void translation_unit_visitor::process_field(
|
||||
|
||||
auto field_type_str =
|
||||
common::to_string(field_type, field_declaration.getASTContext(), false);
|
||||
|
||||
ensure_lambda_type_is_relative(field_type_str);
|
||||
|
||||
class_member field{
|
||||
@@ -2239,11 +2358,11 @@ void translation_unit_visitor::process_field(
|
||||
// Find relationship for the type if the type has not been added
|
||||
// as aggregation
|
||||
if (!template_instantiation_added_as_aggregation) {
|
||||
if ((field_type->getAsCXXRecordDecl() != nullptr) &&
|
||||
field_type->getAsCXXRecordDecl()->getNameAsString().empty()) {
|
||||
if ((field_type->getAsRecordDecl() != nullptr) &&
|
||||
field_type->getAsRecordDecl()->getNameAsString().empty()) {
|
||||
// Relationships to fields whose type is an anonymous nested
|
||||
// struct have to be handled separately here
|
||||
anonymous_struct_relationships_[field_type->getAsCXXRecordDecl()
|
||||
anonymous_struct_relationships_[field_type->getAsRecordDecl()
|
||||
->getID()] =
|
||||
std::make_tuple(
|
||||
field.name(), relationship_hint, field.access());
|
||||
|
||||
@@ -69,6 +69,8 @@ public:
|
||||
|
||||
virtual bool VisitNamespaceDecl(clang::NamespaceDecl *ns);
|
||||
|
||||
virtual bool VisitRecordDecl(clang::RecordDecl *D);
|
||||
|
||||
virtual bool VisitCXXRecordDecl(clang::CXXRecordDecl *d);
|
||||
|
||||
virtual bool VisitEnumDecl(clang::EnumDecl *e);
|
||||
@@ -109,6 +111,9 @@ private:
|
||||
std::unique_ptr<clanguml::class_diagram::model::class_>
|
||||
create_class_declaration(clang::CXXRecordDecl *cls);
|
||||
|
||||
std::unique_ptr<clanguml::class_diagram::model::class_>
|
||||
create_record_declaration(clang::RecordDecl *rec);
|
||||
|
||||
void process_class_declaration(const clang::CXXRecordDecl &cls,
|
||||
clanguml::class_diagram::model::class_ &c);
|
||||
|
||||
@@ -118,6 +123,8 @@ private:
|
||||
void process_class_children(const clang::CXXRecordDecl *cls,
|
||||
clanguml::class_diagram::model::class_ &c);
|
||||
|
||||
void process_record_members(const clang::RecordDecl *cls, class_ &c);
|
||||
|
||||
std::unique_ptr<clanguml::class_diagram::model::class_>
|
||||
process_template_specialization(
|
||||
clang::ClassTemplateSpecializationDecl *cls);
|
||||
@@ -224,6 +231,9 @@ private:
|
||||
|
||||
void ensure_lambda_type_is_relative(std::string ¶meter_type) const;
|
||||
|
||||
void process_record_parent(
|
||||
clang::RecordDecl *cls, class_ &c, const namespace_ &ns);
|
||||
|
||||
void process_function_parameter_find_relatinoships_in_autotype(
|
||||
model::class_ &c, const clang::AutoType *atsp);
|
||||
|
||||
|
||||
@@ -152,7 +152,12 @@ std::string to_string(const clang::QualType &type, const clang::ASTContext &ctx,
|
||||
// it has some default name
|
||||
if (result.empty())
|
||||
result = "(anonymous)";
|
||||
else if (util::contains(result, "unnamed struct")) {
|
||||
else if (util::contains(result, "unnamed struct") ||
|
||||
util::contains(result, "unnamed union")) {
|
||||
result = common::get_tag_name(*type->getAsTagDecl());
|
||||
}
|
||||
else if (util::contains(result, "anonymous struct") ||
|
||||
util::contains(result, "anonymous union")) {
|
||||
result = common::get_tag_name(*type->getAsTagDecl());
|
||||
}
|
||||
|
||||
|
||||
@@ -125,6 +125,8 @@ template <> id_t to_id(const clang::NamespaceDecl &declaration);
|
||||
|
||||
template <> id_t to_id(const clang::CXXRecordDecl &declaration);
|
||||
|
||||
template <> id_t to_id(const clang::RecordDecl &declaration);
|
||||
|
||||
template <> id_t to_id(const clang::EnumDecl &declaration);
|
||||
|
||||
template <> id_t to_id(const clang::TagDecl &declaration);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
file(GLOB_RECURSE TEST_CASE_SOURCES t*/*.cc)
|
||||
file(GLOB_RECURSE TEST_CASE_SOURCES t*/*.cc t*/*.c t*/src/*.c)
|
||||
file(GLOB_RECURSE TEST_CASE_CONFIGS t*/.clang-uml)
|
||||
file(GLOB_RECURSE TEST_CONFIG_YMLS test_config_data/*.yml)
|
||||
|
||||
|
||||
8
tests/t00057/.clang-uml
Normal file
8
tests/t00057/.clang-uml
Normal file
@@ -0,0 +1,8 @@
|
||||
compilation_database_dir: ..
|
||||
output_directory: puml
|
||||
diagrams:
|
||||
t00057_class:
|
||||
type: class
|
||||
glob:
|
||||
- ../../tests/t00057/t00057.c
|
||||
- ../../tests/t00057/src/t00057_impl.c
|
||||
3
tests/t00057/include/t00057.h
Normal file
3
tests/t00057/include/t00057.h
Normal file
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
struct t00057_F;
|
||||
5
tests/t00057/src/t00057_impl.c
Normal file
5
tests/t00057/src/t00057_impl.c
Normal file
@@ -0,0 +1,5 @@
|
||||
#include "../include/t00057.h"
|
||||
|
||||
struct t00057_F {
|
||||
int f1;
|
||||
};
|
||||
39
tests/t00057/t00057.c
Normal file
39
tests/t00057/t00057.c
Normal file
@@ -0,0 +1,39 @@
|
||||
#include "include/t00057.h"
|
||||
|
||||
struct t00057_A {
|
||||
int a1;
|
||||
};
|
||||
|
||||
typedef struct t00057_B {
|
||||
int b1;
|
||||
} t00057_B;
|
||||
|
||||
struct t00057_C {
|
||||
int c1;
|
||||
};
|
||||
|
||||
union t00057_D {
|
||||
int d1;
|
||||
float d2;
|
||||
};
|
||||
|
||||
struct t00057_E {
|
||||
int e;
|
||||
struct {
|
||||
int x;
|
||||
int y;
|
||||
} coordinates;
|
||||
union {
|
||||
int z;
|
||||
double t;
|
||||
} height;
|
||||
};
|
||||
|
||||
struct t00057_R {
|
||||
struct t00057_A a;
|
||||
t00057_B b;
|
||||
struct t00057_C *c;
|
||||
union t00057_D d;
|
||||
struct t00057_E *e;
|
||||
struct t00057_F *f;
|
||||
};
|
||||
60
tests/t00057/test_case.h
Normal file
60
tests/t00057/test_case.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* tests/t00057/test_case.h
|
||||
*
|
||||
* Copyright (c) 2021-2023 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("t00057", "[test-case][class]")
|
||||
{
|
||||
auto [config, db] = load_config("t00057");
|
||||
|
||||
auto diagram = config.diagrams["t00057_class"];
|
||||
|
||||
REQUIRE(diagram->name == "t00057_class");
|
||||
|
||||
auto model = generate_class_diagram(*db, diagram);
|
||||
|
||||
REQUIRE(model->name() == "t00057_class");
|
||||
|
||||
auto puml = generate_class_puml(diagram, *model);
|
||||
AliasMatcher _A(puml);
|
||||
|
||||
REQUIRE_THAT(puml, StartsWith("@startuml"));
|
||||
REQUIRE_THAT(puml, EndsWith("@enduml\n"));
|
||||
|
||||
// Check if all classes exist
|
||||
REQUIRE_THAT(puml, IsClass(_A("t00057_A")));
|
||||
REQUIRE_THAT(puml, IsClass(_A("t00057_B")));
|
||||
REQUIRE_THAT(puml, IsClass(_A("t00057_C")));
|
||||
REQUIRE_THAT(puml, IsUnion(_A("t00057_D")));
|
||||
REQUIRE_THAT(puml, IsClass(_A("t00057_E")));
|
||||
REQUIRE_THAT(puml, IsClass(_A("t00057_F")));
|
||||
REQUIRE_THAT(puml, IsClass(_A("t00057_R")));
|
||||
|
||||
// Check if all relationships exist
|
||||
REQUIRE_THAT(puml, IsAggregation(_A("t00057_R"), _A("t00057_A"), "+a"));
|
||||
REQUIRE_THAT(puml, IsAggregation(_A("t00057_R"), _A("t00057_B"), "+b"));
|
||||
REQUIRE_THAT(puml, IsAssociation(_A("t00057_R"), _A("t00057_C"), "+c"));
|
||||
REQUIRE_THAT(puml, IsAggregation(_A("t00057_R"), _A("t00057_D"), "+d"));
|
||||
REQUIRE_THAT(puml, IsAssociation(_A("t00057_R"), _A("t00057_E"), "+e"));
|
||||
REQUIRE_THAT(puml, IsAssociation(_A("t00057_R"), _A("t00057_F"), "+f"));
|
||||
REQUIRE_THAT(puml,
|
||||
IsAggregation(
|
||||
_A("t00057_E"), _A("t00057_E::(coordinates)"), "+coordinates"));
|
||||
REQUIRE_THAT(puml,
|
||||
IsAggregation(_A("t00057_E"), _A("t00057_E::(height)"), "+height"));
|
||||
|
||||
save_puml(config.output_directory() + "/" + diagram->name + ".puml", puml);
|
||||
}
|
||||
@@ -255,6 +255,7 @@ using namespace clanguml::test::matchers;
|
||||
#include "t00053/test_case.h"
|
||||
#include "t00054/test_case.h"
|
||||
#include "t00055/test_case.h"
|
||||
#include "t00057/test_case.h"
|
||||
|
||||
///
|
||||
/// Sequence diagram tests
|
||||
|
||||
@@ -266,6 +266,13 @@ ContainsMatcher IsClass(std::string const &str,
|
||||
return ContainsMatcher(CasedString("class " + str, caseSensitivity));
|
||||
}
|
||||
|
||||
ContainsMatcher IsUnion(std::string const &str,
|
||||
CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes)
|
||||
{
|
||||
return ContainsMatcher(
|
||||
CasedString("class " + str + " <<union>>", caseSensitivity));
|
||||
}
|
||||
|
||||
ContainsMatcher IsClassTemplate(std::string const &str,
|
||||
std::string const &tmplt,
|
||||
CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes)
|
||||
|
||||
@@ -162,6 +162,9 @@ test_cases:
|
||||
- name: t00055
|
||||
title: Test case for `row` and `column` layout hints
|
||||
description:
|
||||
- name: t00057
|
||||
title: Test case C99/C11 translation units with structs and unions
|
||||
description:
|
||||
Sequence diagrams:
|
||||
- name: t20001
|
||||
title: Basic sequence diagram test case
|
||||
|
||||
Reference in New Issue
Block a user