diff --git a/CHANGELOG.md b/CHANGELOG.md index 417ccd77..e9300d77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/README.md b/README.md index c55653d8..da7caf82 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/class_diagram/generators/plantuml/class_diagram_generator.cc b/src/class_diagram/generators/plantuml/class_diagram_generator.cc index 6c3b0541..8b18db8b 100644 --- a/src/class_diagram/generators/plantuml/class_diagram_generator.cc +++ b/src/class_diagram/generators/plantuml/class_diagram_generator.cc @@ -116,6 +116,10 @@ void generator::generate(const class_ &c, std::ostream &ostr) const ostr << class_type << " " << c.alias(); + if (c.is_union()) + ostr << " " + << "<>"; + if (m_config.generate_links) { common_generator::generate_link(ostr, c); } diff --git a/src/class_diagram/model/class.h b/src/class_diagram/model/class.h index 0fcbfa76..97b55e19 100644 --- a/src/class_diagram/model/class.h +++ b/src/class_diagram/model/class.h @@ -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> &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 members_; std::vector methods_; std::vector bases_; diff --git a/src/class_diagram/visitor/translation_unit_visitor.cc b/src/class_diagram/visitor/translation_unit_visitor.cc index 99071c7e..62ddcafa 100644 --- a/src/class_diagram/visitor/translation_unit_visitor.cc +++ b/src/class_diagram/visitor/translation_unit_visitor.cc @@ -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(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 translation_unit_visitor::create_record_declaration( + clang::RecordDecl *rec) +{ + assert(rec != nullptr); + + auto record_ptr{std::make_unique(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 translation_unit_visitor::create_class_declaration( clang::CXXRecordDecl *cls) { @@ -392,14 +485,37 @@ std::unique_ptr 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 id_opt; @@ -435,7 +551,8 @@ std::unique_ptr 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 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()); diff --git a/src/class_diagram/visitor/translation_unit_visitor.h b/src/class_diagram/visitor/translation_unit_visitor.h index 9313087c..d275c2e8 100644 --- a/src/class_diagram/visitor/translation_unit_visitor.h +++ b/src/class_diagram/visitor/translation_unit_visitor.h @@ -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 create_class_declaration(clang::CXXRecordDecl *cls); + std::unique_ptr + 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 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); diff --git a/src/common/clang_utils.cc b/src/common/clang_utils.cc index 57cea568..ce94c1ff 100644 --- a/src/common/clang_utils.cc +++ b/src/common/clang_utils.cc @@ -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()); } diff --git a/src/common/clang_utils.h b/src/common/clang_utils.h index 1c162ae5..6e40ee6d 100644 --- a/src/common/clang_utils.h +++ b/src/common/clang_utils.h @@ -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); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ded8a48b..f9bf22c5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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) diff --git a/tests/t00057/.clang-uml b/tests/t00057/.clang-uml new file mode 100644 index 00000000..6ce6b2aa --- /dev/null +++ b/tests/t00057/.clang-uml @@ -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 \ No newline at end of file diff --git a/tests/t00057/include/t00057.h b/tests/t00057/include/t00057.h new file mode 100644 index 00000000..129663bf --- /dev/null +++ b/tests/t00057/include/t00057.h @@ -0,0 +1,3 @@ +#pragma once + +struct t00057_F; \ No newline at end of file diff --git a/tests/t00057/src/t00057_impl.c b/tests/t00057/src/t00057_impl.c new file mode 100644 index 00000000..c7a4071c --- /dev/null +++ b/tests/t00057/src/t00057_impl.c @@ -0,0 +1,5 @@ +#include "../include/t00057.h" + +struct t00057_F { + int f1; +}; \ No newline at end of file diff --git a/tests/t00057/t00057.c b/tests/t00057/t00057.c new file mode 100644 index 00000000..2c025cd4 --- /dev/null +++ b/tests/t00057/t00057.c @@ -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; +}; diff --git a/tests/t00057/test_case.h b/tests/t00057/test_case.h new file mode 100644 index 00000000..02b89d93 --- /dev/null +++ b/tests/t00057/test_case.h @@ -0,0 +1,60 @@ +/** + * tests/t00057/test_case.h + * + * Copyright (c) 2021-2023 Bartek Kryza + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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); +} \ No newline at end of file diff --git a/tests/test_cases.cc b/tests/test_cases.cc index c8e8ef3b..a1d4d618 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -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 diff --git a/tests/test_cases.h b/tests/test_cases.h index 388e6ae7..99894176 100644 --- a/tests/test_cases.h +++ b/tests/test_cases.h @@ -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 + " <>", caseSensitivity)); +} + ContainsMatcher IsClassTemplate(std::string const &str, std::string const &tmplt, CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes) diff --git a/tests/test_cases.yaml b/tests/test_cases.yaml index df742152..0cbdbea1 100644 --- a/tests/test_cases.yaml +++ b/tests/test_cases.yaml @@ -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