Added support for 'class' diagrams from C99/C11 translation units (#97)

This commit is contained in:
Bartek Kryza
2023-02-19 18:49:59 +01:00
parent 4afb1daef0
commit 6341a62857
17 changed files with 301 additions and 30 deletions

View File

@@ -1,5 +1,6 @@
# CHANGELOG # CHANGELOG
* Added support for plain C11 translation units (#97)
* Added 'row' and 'column' layout hints for aligning elements (#90) * Added 'row' and 'column' layout hints for aligning elements (#90)
* Added 'together' layout hint for grouping elements (#43) * Added 'together' layout hint for grouping elements (#43)
* Enabled adding notes to class methods and members (#87) * Enabled adding notes to class methods and members (#87)

View File

@@ -28,6 +28,7 @@ Main features supported so far include:
* Diagram content filtering based on namespaces, elements and relationships * Diagram content filtering based on namespaces, elements and relationships
* Optional package generation from namespaces * Optional package generation from namespaces
* Interactive links to online code to classes, methods and class fields in SVG diagrams * 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** * **Sequence diagram generation**
* Generation of sequence diagram from specific method or function * Generation of sequence diagram from specific method or function
* Generation of loop and conditional statements * Generation of loop and conditional statements

View File

@@ -116,6 +116,10 @@ void generator::generate(const class_ &c, std::ostream &ostr) const
ostr << class_type << " " << c.alias(); ostr << class_type << " " << c.alias();
if (c.is_union())
ostr << " "
<< "<<union>>";
if (m_config.generate_links) { if (m_config.generate_links) {
common_generator<diagram_config, diagram_model>::generate_link(ostr, c); common_generator<diagram_config, diagram_model>::generate_link(ostr, c);
} }

View File

@@ -54,6 +54,12 @@ public:
bool is_template_instantiation() const; bool is_template_instantiation() const;
void is_template_instantiation(bool is_template_instantiation); 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_member(class_member &&member);
void add_method(class_method &&method); void add_method(class_method &&method);
void add_parent(class_parent &&parent); void add_parent(class_parent &&parent);
@@ -73,10 +79,6 @@ public:
bool is_abstract() const; bool is_abstract() const;
bool is_alias() const { return is_alias_; }
void is_alias(bool alias) { is_alias_ = alias; }
void find_relationships( void find_relationships(
std::vector<std::pair<std::string, common::model::relationship_t>> std::vector<std::pair<std::string, common::model::relationship_t>>
&nested_relationships); &nested_relationships);
@@ -89,6 +91,7 @@ private:
bool is_template_{false}; bool is_template_{false};
bool is_template_instantiation_{false}; bool is_template_instantiation_{false};
bool is_alias_{false}; bool is_alias_{false};
bool is_union_{false};
std::vector<class_member> members_; std::vector<class_member> members_;
std::vector<class_method> methods_; std::vector<class_method> methods_;
std::vector<class_parent> bases_; std::vector<class_parent> bases_;

View File

@@ -310,6 +310,63 @@ bool translation_unit_visitor::VisitClassTemplateDecl(
return true; 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) bool translation_unit_visitor::VisitCXXRecordDecl(clang::CXXRecordDecl *cls)
{ {
// Skip system headers // Skip system headers
@@ -383,6 +440,42 @@ bool translation_unit_visitor::VisitCXXRecordDecl(clang::CXXRecordDecl *cls)
return true; 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( std::unique_ptr<class_> translation_unit_visitor::create_class_declaration(
clang::CXXRecordDecl *cls) clang::CXXRecordDecl *cls)
{ {
@@ -392,14 +485,37 @@ std::unique_ptr<class_> translation_unit_visitor::create_class_declaration(
auto &c = *c_ptr; auto &c = *c_ptr;
// TODO: refactor to method get_qualified_name() // TODO: refactor to method get_qualified_name()
auto qualified_name = auto qualified_name = cls->getQualifiedNameAsString();
cls->getQualifiedNameAsString(); // common::get_qualified_name(*cls);
if (!diagram().should_include(qualified_name)) if (!diagram().should_include(qualified_name))
return {}; return {};
auto ns = common::get_tag_namespace(*cls); 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(); const auto *parent = cls->getParent();
std::optional<common::model::diagram_element::id_t> id_opt; 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); assert(parent_class);
c.set_namespace(ns); c.set_namespace(ns);
if (cls->getNameAsString().empty()) { const auto cls_name = cls->getNameAsString();
if (cls_name.empty()) {
// Nested structs can be anonymous // Nested structs can be anonymous
if (anonymous_struct_relationships_.count(cls->getID()) > 0) { if (anonymous_struct_relationships_.count(cls->getID()) > 0) {
const auto &[label, hint, access] = const auto &[label, hint, access] =
@@ -467,23 +584,6 @@ std::unique_ptr<class_> translation_unit_visitor::create_class_declaration(
c.nested(true); 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( 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( void translation_unit_visitor::process_class_children(
const clang::CXXRecordDecl *cls, class_ &c) 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()); const auto target_id = common::to_id(*type->getAsCXXRecordDecl());
relationships.emplace_back(target_id, relationship_hint); relationships.emplace_back(target_id, relationship_hint);
result = true; result = true;
} }
else {
const auto target_id = common::to_id(*type->getAsRecordDecl());
relationships.emplace_back(target_id, relationship_hint);
result = true;
}
} }
return result; return result;
@@ -2092,6 +2207,9 @@ bool translation_unit_visitor::build_template_instantiation_add_base_classes(
void translation_unit_visitor::process_field( void translation_unit_visitor::process_field(
const clang::FieldDecl &field_declaration, class_ &c) const clang::FieldDecl &field_declaration, class_ &c)
{ {
LOG_DBG(
"== Visiting record member {}", field_declaration.getNameAsString());
// Default hint for relationship is aggregation // Default hint for relationship is aggregation
auto relationship_hint = relationship_t::kAggregation; auto relationship_hint = relationship_t::kAggregation;
// If the first type of the template instantiation of this field type // 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 = auto field_type_str =
common::to_string(field_type, field_declaration.getASTContext(), false); common::to_string(field_type, field_declaration.getASTContext(), false);
ensure_lambda_type_is_relative(field_type_str); ensure_lambda_type_is_relative(field_type_str);
class_member field{ 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 // Find relationship for the type if the type has not been added
// as aggregation // as aggregation
if (!template_instantiation_added_as_aggregation) { if (!template_instantiation_added_as_aggregation) {
if ((field_type->getAsCXXRecordDecl() != nullptr) && if ((field_type->getAsRecordDecl() != nullptr) &&
field_type->getAsCXXRecordDecl()->getNameAsString().empty()) { field_type->getAsRecordDecl()->getNameAsString().empty()) {
// Relationships to fields whose type is an anonymous nested // Relationships to fields whose type is an anonymous nested
// struct have to be handled separately here // struct have to be handled separately here
anonymous_struct_relationships_[field_type->getAsCXXRecordDecl() anonymous_struct_relationships_[field_type->getAsRecordDecl()
->getID()] = ->getID()] =
std::make_tuple( std::make_tuple(
field.name(), relationship_hint, field.access()); field.name(), relationship_hint, field.access());

View File

@@ -69,6 +69,8 @@ public:
virtual bool VisitNamespaceDecl(clang::NamespaceDecl *ns); virtual bool VisitNamespaceDecl(clang::NamespaceDecl *ns);
virtual bool VisitRecordDecl(clang::RecordDecl *D);
virtual bool VisitCXXRecordDecl(clang::CXXRecordDecl *d); virtual bool VisitCXXRecordDecl(clang::CXXRecordDecl *d);
virtual bool VisitEnumDecl(clang::EnumDecl *e); virtual bool VisitEnumDecl(clang::EnumDecl *e);
@@ -109,6 +111,9 @@ private:
std::unique_ptr<clanguml::class_diagram::model::class_> std::unique_ptr<clanguml::class_diagram::model::class_>
create_class_declaration(clang::CXXRecordDecl *cls); 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, void process_class_declaration(const clang::CXXRecordDecl &cls,
clanguml::class_diagram::model::class_ &c); clanguml::class_diagram::model::class_ &c);
@@ -118,6 +123,8 @@ private:
void process_class_children(const clang::CXXRecordDecl *cls, void process_class_children(const clang::CXXRecordDecl *cls,
clanguml::class_diagram::model::class_ &c); clanguml::class_diagram::model::class_ &c);
void process_record_members(const clang::RecordDecl *cls, class_ &c);
std::unique_ptr<clanguml::class_diagram::model::class_> std::unique_ptr<clanguml::class_diagram::model::class_>
process_template_specialization( process_template_specialization(
clang::ClassTemplateSpecializationDecl *cls); clang::ClassTemplateSpecializationDecl *cls);
@@ -224,6 +231,9 @@ private:
void ensure_lambda_type_is_relative(std::string &parameter_type) const; void ensure_lambda_type_is_relative(std::string &parameter_type) const;
void process_record_parent(
clang::RecordDecl *cls, class_ &c, const namespace_ &ns);
void process_function_parameter_find_relatinoships_in_autotype( void process_function_parameter_find_relatinoships_in_autotype(
model::class_ &c, const clang::AutoType *atsp); model::class_ &c, const clang::AutoType *atsp);

View File

@@ -152,7 +152,12 @@ std::string to_string(const clang::QualType &type, const clang::ASTContext &ctx,
// it has some default name // it has some default name
if (result.empty()) if (result.empty())
result = "(anonymous)"; 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()); result = common::get_tag_name(*type->getAsTagDecl());
} }

View File

@@ -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::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::EnumDecl &declaration);
template <> id_t to_id(const clang::TagDecl &declaration); template <> id_t to_id(const clang::TagDecl &declaration);

View File

@@ -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_CASE_CONFIGS t*/.clang-uml)
file(GLOB_RECURSE TEST_CONFIG_YMLS test_config_data/*.yml) file(GLOB_RECURSE TEST_CONFIG_YMLS test_config_data/*.yml)

8
tests/t00057/.clang-uml Normal file
View 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

View File

@@ -0,0 +1,3 @@
#pragma once
struct t00057_F;

View File

@@ -0,0 +1,5 @@
#include "../include/t00057.h"
struct t00057_F {
int f1;
};

39
tests/t00057/t00057.c Normal file
View 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
View 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);
}

View File

@@ -255,6 +255,7 @@ using namespace clanguml::test::matchers;
#include "t00053/test_case.h" #include "t00053/test_case.h"
#include "t00054/test_case.h" #include "t00054/test_case.h"
#include "t00055/test_case.h" #include "t00055/test_case.h"
#include "t00057/test_case.h"
/// ///
/// Sequence diagram tests /// Sequence diagram tests

View File

@@ -266,6 +266,13 @@ ContainsMatcher IsClass(std::string const &str,
return ContainsMatcher(CasedString("class " + str, caseSensitivity)); 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, ContainsMatcher IsClassTemplate(std::string const &str,
std::string const &tmplt, std::string const &tmplt,
CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes) CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes)

View File

@@ -162,6 +162,9 @@ test_cases:
- name: t00055 - name: t00055
title: Test case for `row` and `column` layout hints title: Test case for `row` and `column` layout hints
description: description:
- name: t00057
title: Test case C99/C11 translation units with structs and unions
description:
Sequence diagrams: Sequence diagrams:
- name: t20001 - name: t20001
title: Basic sequence diagram test case title: Basic sequence diagram test case