diff --git a/src/class_diagram/model/diagram.cc b/src/class_diagram/model/diagram.cc index a062cad2..270bef0b 100644 --- a/src/class_diagram/model/diagram.cc +++ b/src/class_diagram/model/diagram.cc @@ -249,6 +249,11 @@ void diagram::remove_redundant_dependencies() } } +bool diagram::is_empty() const +{ + return element_view::is_empty() && + element_view::is_empty() && element_view::is_empty(); +} } // namespace clanguml::class_diagram::model namespace clanguml::common::model { diff --git a/src/class_diagram/model/diagram.h b/src/class_diagram/model/diagram.h index 835035a4..3415e12e 100644 --- a/src/class_diagram/model/diagram.h +++ b/src/class_diagram/model/diagram.h @@ -249,6 +249,13 @@ public: */ inja::json context() const override; + /** + * @brief Check whether the diagram is empty + * + * @return True, if diagram is empty + */ + bool is_empty() const override; + private: template bool add_with_namespace_path(std::unique_ptr &&e); diff --git a/src/cli/cli_handler.cc b/src/cli/cli_handler.cc index 7bc94d21..36d343ca 100644 --- a/src/cli/cli_handler.cc +++ b/src/cli/cli_handler.cc @@ -107,6 +107,8 @@ cli_flow_t cli_handler::parse(int argc, const char **argv) "Query the specific compiler driver to extract system paths and add to " "compile commands (e.g. arm-none-eabi-g++)"); #endif + app.add_flag("--allow-empty-diagrams", allow_empty_diagrams, + "Do not raise an error when generated diagram model is empty"); app.add_option( "--add-class-diagram", add_class_diagram, "Add class diagram config"); app.add_option("--add-sequence-diagram", add_sequence_diagram, @@ -302,6 +304,10 @@ cli_flow_t cli_handler::handle_post_config_options() LOG_INFO("Loaded clang-uml config from {}", config_path); + if (allow_empty_diagrams) { + config.allow_empty_diagrams.set(true); + } + // // Override selected config options from command line // diff --git a/src/cli/cli_handler.h b/src/cli/cli_handler.h index 2aec25a7..ae222369 100644 --- a/src/cli/cli_handler.h +++ b/src/cli/cli_handler.h @@ -165,6 +165,7 @@ public: bool list_diagrams{false}; bool quiet{false}; bool initialize{false}; + bool allow_empty_diagrams{false}; std::optional> add_compile_flag; std::optional> remove_compile_flag; #if !defined(_WIN32) diff --git a/src/common/generators/generators.cc b/src/common/generators/generators.cc index 2a3f1e5b..4799e8c0 100644 --- a/src/common/generators/generators.cc +++ b/src/common/generators/generators.cc @@ -95,11 +95,17 @@ void generate_diagram_select_generator(const std::string &od, using diagram_generator = typename diagram_generator_t::type; + std::stringstream buffer; + buffer << diagram_generator( + dynamic_cast(*diagram), *model); + + // Only open the file after the diagram has been generated successfully + // in order not to overwrite previous diagram in case of failure auto path = std::filesystem::path{od} / fmt::format("{}.{}", name, GeneratorTag::extension); std::ofstream ofs; ofs.open(path, std::ofstream::out | std::ofstream::trunc); - ofs << diagram_generator(dynamic_cast(*diagram), *model); + ofs << buffer.str(); ofs.close(); @@ -258,11 +264,12 @@ void generate_diagrams(const std::vector &diagram_names, if (indicator) indicator->complete(name); } - catch (std::exception &e) { + catch (const std::exception &e) { if (indicator) indicator->fail(name); - LOG_ERROR(e.what()); + LOG_ERROR( + "ERROR: Failed to generate diagram {}: {}", name, e.what()); } }; diff --git a/src/common/generators/json/generator.h b/src/common/generators/json/generator.h index d76037a0..dea6b3d4 100644 --- a/src/common/generators/json/generator.h +++ b/src/common/generators/json/generator.h @@ -107,11 +107,19 @@ std::ostream &operator<<( template void generator::generate(std::ostream &ostr) const { + const auto &config = generators::generator::config(); + const auto &model = generators::generator::model(); + + if (!config.allow_empty_diagrams() && model.is_empty()) { + throw clanguml::error::empty_diagram_error{ + "Diagram configuration resulted in empty diagram."}; + } + nlohmann::json j; - j["name"] = generators::generator::model().name(); - j["diagram_type"] = to_string(generators::generator::model().type()); - if (generators::generator::config().title) { - j["title"] = generators::generator::config().title(); + j["name"] = model.name(); + j["diagram_type"] = to_string(model.type()); + if (config.title) { + j["title"] = config.title(); } generate_diagram(j); diff --git a/src/common/generators/mermaid/generator.h b/src/common/generators/mermaid/generator.h index 92a72b4a..9333d8b4 100644 --- a/src/common/generators/mermaid/generator.h +++ b/src/common/generators/mermaid/generator.h @@ -250,6 +250,13 @@ template void generator::generate(std::ostream &ostr) const { const auto &config = generators::generator::config(); + const auto &model = generators::generator::model(); + + if (!config.allow_empty_diagrams() && model.is_empty() && + config.mermaid().before.empty() && config.mermaid().after.empty()) { + throw clanguml::error::empty_diagram_error{ + "Diagram configuration resulted in empty diagram."}; + } update_context(); diff --git a/src/common/generators/plantuml/generator.h b/src/common/generators/plantuml/generator.h index 85406e08..e21f474e 100644 --- a/src/common/generators/plantuml/generator.h +++ b/src/common/generators/plantuml/generator.h @@ -21,6 +21,7 @@ #include "common/model/diagram_filter.h" #include "common/model/relationship.h" #include "config/config.h" +#include "error.h" #include "util/error.h" #include "util/util.h" #include "version.h" @@ -265,9 +266,16 @@ template void generator::generate(std::ostream &ostr) const { const auto &config = generators::generator::config(); + const auto &model = generators::generator::model(); update_context(); + if (!config.allow_empty_diagrams() && model.is_empty() && + config.puml().before.empty() && config.puml().after.empty()) { + throw clanguml::error::empty_diagram_error{ + "Diagram configuration resulted in empty diagram."}; + } + ostr << "@startuml" << '\n'; generate_title(ostr); diff --git a/src/common/model/diagram.h b/src/common/model/diagram.h index 28f40f47..89161d6d 100644 --- a/src/common/model/diagram.h +++ b/src/common/model/diagram.h @@ -164,6 +164,13 @@ public: */ virtual inja::json context() const = 0; + /** + * @brief Check whether the diagram is empty + * + * @return True, if diagram is empty + */ + virtual bool is_empty() const = 0; + private: std::string name_; std::unique_ptr filter_; diff --git a/src/common/model/element_view.h b/src/common/model/element_view.h index b7a25c79..bd3ad090 100644 --- a/src/common/model/element_view.h +++ b/src/common/model/element_view.h @@ -69,6 +69,13 @@ public: return {}; } + /** + * @brief Check whether the element view is empty + * + * @return True, if the view does not contain any elements + */ + bool is_empty() const { return elements_.empty(); } + private: reference_vector elements_; }; diff --git a/src/config/config.cc b/src/config/config.cc index 0a59d7d8..0eb83f91 100644 --- a/src/config/config.cc +++ b/src/config/config.cc @@ -237,6 +237,7 @@ void inheritable_diagram_options::inherit( parent.generate_condition_statements); debug_mode.override(parent.debug_mode); generate_metadata.override(parent.generate_metadata); + allow_empty_diagrams.override(parent.allow_empty_diagrams); type_aliases.override(parent.type_aliases); } diff --git a/src/config/config.h b/src/config/config.h index f4ece81a..e76a8bef 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -577,6 +577,7 @@ struct inheritable_diagram_options { "message_comment_width", clanguml::util::kDefaultMessageCommentWidth}; option debug_mode{"debug_mode", false}; option generate_metadata{"generate_metadata", true}; + option allow_empty_diagrams{"allow_empty_diagrams", false}; protected: // This is the relative path with respect to the `base_directory`, diff --git a/src/config/schema.h b/src/config/schema.h index fa76104c..2b0eabb1 100644 --- a/src/config/schema.h +++ b/src/config/schema.h @@ -306,6 +306,7 @@ root: query_driver: !optional string add_compile_flags: !optional [string] remove_compile_flags: !optional [string] + allow_empty_diagrams: !optional bool diagram_templates: !optional diagram_templates_t diagrams: !required map_t # diff --git a/src/config/yaml_decoders.cc b/src/config/yaml_decoders.cc index bcf5f329..a63271d0 100644 --- a/src/config/yaml_decoders.cc +++ b/src/config/yaml_decoders.cc @@ -820,6 +820,7 @@ template <> struct convert { get_option(node, rhs.using_module); get_option(node, rhs.output_directory); get_option(node, rhs.query_driver); + get_option(node, rhs.allow_empty_diagrams); get_option(node, rhs.compilation_database_dir); get_option(node, rhs.add_compile_flags); get_option(node, rhs.remove_compile_flags); diff --git a/src/include_diagram/model/diagram.cc b/src/include_diagram/model/diagram.cc index 9e5b062f..0b385581 100644 --- a/src/include_diagram/model/diagram.cc +++ b/src/include_diagram/model/diagram.cc @@ -151,6 +151,9 @@ inja::json diagram::context() const return ctx; } + +bool diagram::is_empty() const { return element_view::is_empty(); } + } // namespace clanguml::include_diagram::model namespace clanguml::common::model { diff --git a/src/include_diagram/model/diagram.h b/src/include_diagram/model/diagram.h index f9b0dabc..e8a1deba 100644 --- a/src/include_diagram/model/diagram.h +++ b/src/include_diagram/model/diagram.h @@ -131,6 +131,13 @@ public: const common::model::namespace_ &ns) const override; inja::json context() const override; + + /** + * @brief Check whether the diagram is empty + * + * @return True, if diagram is empty + */ + bool is_empty() const override; }; template diff --git a/src/package_diagram/model/diagram.cc b/src/package_diagram/model/diagram.cc index 4ba5e6f0..21a903ed 100644 --- a/src/package_diagram/model/diagram.cc +++ b/src/package_diagram/model/diagram.cc @@ -73,6 +73,8 @@ inja::json diagram::context() const return ctx; } + +bool diagram::is_empty() const { return element_view::is_empty(); } } // namespace clanguml::package_diagram::model namespace clanguml::common::model { diff --git a/src/package_diagram/model/diagram.h b/src/package_diagram/model/diagram.h index 335a5456..f29c9c66 100644 --- a/src/package_diagram/model/diagram.h +++ b/src/package_diagram/model/diagram.h @@ -157,6 +157,13 @@ public: */ inja::json context() const override; + /** + * @brief Check whether the diagram is empty + * + * @return True, if diagram is empty + */ + bool is_empty() const override; + private: /** * @brief Add element using module as diagram path diff --git a/src/sequence_diagram/model/diagram.cc b/src/sequence_diagram/model/diagram.cc index 45d2f97a..176b2253 100644 --- a/src/sequence_diagram/model/diagram.cc +++ b/src/sequence_diagram/model/diagram.cc @@ -399,6 +399,11 @@ std::vector diagram::get_all_from_to_message_chains( return message_chains_unique; } +bool diagram::is_empty() const +{ + return sequences_.empty() || participants_.empty(); +} + void diagram::print() const { LOG_TRACE(" --- Participants ---"); diff --git a/src/sequence_diagram/model/diagram.h b/src/sequence_diagram/model/diagram.h index e075a04f..3b485b4d 100644 --- a/src/sequence_diagram/model/diagram.h +++ b/src/sequence_diagram/model/diagram.h @@ -280,6 +280,13 @@ public: */ void finalize() override; + /** + * @brief Check whether the diagram is empty + * + * @return True, if diagram is empty + */ + bool is_empty() const override; + private: /** * This method checks the last messages in sequence (current_messages), diff --git a/src/util/error.h b/src/util/error.h index 97ccf513..bd870dd8 100644 --- a/src/util/error.h +++ b/src/util/error.h @@ -36,4 +36,8 @@ class compilation_database_error : public std::runtime_error { using std::runtime_error::runtime_error; }; +class empty_diagram_error : public std::runtime_error { + using std::runtime_error::runtime_error; +}; + } // namespace clanguml::error diff --git a/tests/t90000/.clang-uml b/tests/t90000/.clang-uml index 9f020697..3e8100be 100644 --- a/tests/t90000/.clang-uml +++ b/tests/t90000/.clang-uml @@ -1,3 +1,4 @@ +allow_empty_diagrams: true diagrams: t90000_class: type: class diff --git a/tests/t90001/.clang-uml b/tests/t90001/.clang-uml new file mode 100644 index 00000000..63ad4c66 --- /dev/null +++ b/tests/t90001/.clang-uml @@ -0,0 +1,24 @@ +diagrams: + t90001_class: + type: class + include: + namespaces: + - no_such_namespace + t90001_sequence: + type: sequence + include: + namespaces: + - no_such_namespace + from: + - function: "nowhere" + t90001_include: + type: include + include: + namespaces: + - no_such_namespace + t90001_package: + type: package + include: + namespaces: + - no_such_namespace + diff --git a/tests/t90001/t90001.cc b/tests/t90001/t90001.cc new file mode 100644 index 00000000..cb51bff9 --- /dev/null +++ b/tests/t90001/t90001.cc @@ -0,0 +1,88 @@ +#include + +namespace clanguml { +namespace t90001 { + +namespace ns1 { +/// \brief This is class A +class A { +public: + /// Abstract foo_a + virtual void foo_a() = 0; + /// Abstract foo_c + virtual void foo_c() = 0; +}; + +/// \brief This is class B +class B : public A { +public: + virtual void foo_a() override { } +}; + +/// @brief This is class C - class C has a long comment +/// +/// Vivamus integer non suscipit taciti mus etiam at primis tempor sagittis sit, +/// euismod libero facilisi aptent elementum felis blandit cursus gravida sociis +/// erat ante, eleifend lectus nullam dapibus netus feugiat curae curabitur est +/// ad. +class C : public A { +public: + /// Do nothing unless override is provided + virtual void foo_c() override { } +}; + +/// This is class D +/// which is a little like B +/// and a little like C +class D : public B, public C { +public: + /** + * Forward foo_a + */ + void foo_a() override + { + for (auto a : as) + a->foo_a(); + } + + /** + * Forward foo_c + */ + void foo_c() override + { + for (auto a : as) + a->foo_c(); + } + +private: + /// All the A pointers + std::vector as; +}; + +class E : virtual public B, public virtual C { +public: + /// + /// Forward foo_a + /// + void foo_a() override + { + for (auto a : as) + a->foo_a(); + } + + /// + /// Forward foo_c + /// + void foo_c() override + { + for (auto a : as) + a->foo_c(); + } + +private: + /// All the A pointers + std::vector as; +}; +} // namespace ns1 +} // namespace t90001 +} // namespace clanguml diff --git a/tests/t90001/test_case.h b/tests/t90001/test_case.h new file mode 100644 index 00000000..812f5a0d --- /dev/null +++ b/tests/t90001/test_case.h @@ -0,0 +1,92 @@ +/** + * tests/t90001/test_case.cc + * + * Copyright (c) 2021-2024 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("t90001", "[test-case][config]") +{ + using clanguml::error::empty_diagram_error; + + auto [config, db] = load_config("t90001"); + + { + auto diagram = config.diagrams["t90001_class"]; + + auto model = generate_class_diagram(*db, diagram); + + REQUIRE(model->is_empty()); + + REQUIRE(model->name() == "t90001_class"); + + REQUIRE_THROWS_AS( + generate_class_puml(diagram, *model), empty_diagram_error); + REQUIRE_THROWS_AS( + generate_class_json(diagram, *model), empty_diagram_error); + REQUIRE_THROWS_AS( + generate_class_mermaid(diagram, *model), empty_diagram_error); + } + + { + auto diagram = config.diagrams["t90001_sequence"]; + + auto model = generate_sequence_diagram(*db, diagram); + + REQUIRE(model->is_empty()); + + REQUIRE(model->name() == "t90001_sequence"); + + REQUIRE_THROWS_AS( + generate_sequence_puml(diagram, *model), empty_diagram_error); + REQUIRE_THROWS_AS( + generate_sequence_json(diagram, *model), empty_diagram_error); + REQUIRE_THROWS_AS( + generate_sequence_mermaid(diagram, *model), empty_diagram_error); + } + + { + auto diagram = config.diagrams["t90001_package"]; + + auto model = generate_package_diagram(*db, diagram); + + REQUIRE(model->is_empty()); + + REQUIRE(model->name() == "t90001_package"); + + REQUIRE_THROWS_AS( + generate_package_puml(diagram, *model), empty_diagram_error); + REQUIRE_THROWS_AS( + generate_package_json(diagram, *model), empty_diagram_error); + REQUIRE_THROWS_AS( + generate_package_mermaid(diagram, *model), empty_diagram_error); + } + + { + auto diagram = config.diagrams["t90001_include"]; + + auto model = generate_include_diagram(*db, diagram); + + REQUIRE(model->is_empty()); + + REQUIRE(model->name() == "t90001_include"); + + REQUIRE_THROWS_AS( + generate_include_puml(diagram, *model), empty_diagram_error); + REQUIRE_THROWS_AS( + generate_include_json(diagram, *model), empty_diagram_error); + REQUIRE_THROWS_AS( + generate_include_mermaid(diagram, *model), empty_diagram_error); + } +} diff --git a/tests/test_cases.cc b/tests/test_cases.cc index 7f5f51b7..66316b09 100644 --- a/tests/test_cases.cc +++ b/tests/test_cases.cc @@ -501,6 +501,7 @@ using namespace clanguml::test::matchers; /// Other tests (e.g. configuration file) /// #include "t90000/test_case.h" +#include "t90001/test_case.h" /// /// Main test function diff --git a/uml/class/architecture_visitors_class.yml b/uml/class/architecture_visitors_class.yml index a17ee7c3..b12d9740 100644 --- a/uml/class/architecture_visitors_class.yml +++ b/uml/class/architecture_visitors_class.yml @@ -12,6 +12,3 @@ include: exclude: access: [ public, protected, private ] relationships: [ dependency ] -plantuml: - before: - - title clang-uml top level architecture - AST visitors