0.6.0
C++ to UML diagram generator based on Clang
Loading...
Searching...
No Matches
generators.cc
Go to the documentation of this file.
1/**
2 * @file src/common/generators/generators.cc
3 *
4 * Copyright (c) 2021-2025 Bartek Kryza <bkryza@gmail.com>
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19#include "generators.h"
20
21#include "progress_indicator.h"
22
25 inja::json &context, const std::string &prefix)
26{
27 if (!context.contains("element"))
28 return;
29
30 if (!context["element"].contains("source"))
31 return;
32
33 auto &source = context["element"]["source"];
34
35 if (source.at("path").empty())
36 return;
37
38 auto path = std::filesystem::path{source.at("path").get<std::string>()};
39 auto prefix_path = std::filesystem::path(prefix);
40 if (path.is_absolute() && util::is_relative_to(path, prefix_path)) {
41 source["path"] = relative(path, prefix_path);
42 return;
43 }
44}
45
47 const std::vector<std::string> &diagram_names,
49 const std::vector<std::string> &compilation_database_files,
50 std::map<std::string, std::vector<std::string>> &translation_units_map)
51{
52 for (const auto &[name, diagram] : config.diagrams) {
53 // If there are any specific diagram names provided on the command line,
54 // and this diagram is not in that list - skip it
55 if (!diagram_names.empty() && !util::contains(diagram_names, name))
56 continue;
57
58 translation_units_map[name] =
59 diagram->glob_translation_units(compilation_database_files);
60
61 LOG_DBG("Found {} translation units for diagram '{}'",
62 translation_units_map.at(name).size(), name);
63 }
64}
65
67 std::shared_ptr<config::diagram> diagram_config)
68{
69 std::string cmd;
70 switch (generator_type) {
72 cmd = diagram_config->puml().cmd;
73 break;
75 cmd = diagram_config->mermaid().cmd;
76 break;
77 default:
78 return;
79 };
80
81 if (cmd.empty())
82 throw std::runtime_error(
83 fmt::format("No render command template provided for {} diagrams",
84 to_string(diagram_config->type())));
85
86 util::replace_all(cmd, "{}", diagram_config->name);
87
88 LOG_INFO("Rendering diagram {} using {}", diagram_config->name,
89 to_string(generator_type));
90
92}
93
94namespace detail {
95
96template <typename DiagramConfig, typename GeneratorTag, typename DiagramModel>
97void generate_diagram_select_generator(const std::string &od,
98 const std::string &name, std::shared_ptr<clanguml::config::diagram> diagram,
99 const DiagramModel &model)
100{
101 using diagram_generator =
103
104 if constexpr (!std::is_same_v<diagram_generator, not_supported>) {
105
106 std::stringstream buffer;
107 buffer << diagram_generator(
108 dynamic_cast<DiagramConfig &>(*diagram), *model);
109
110 // Only open the file after the diagram has been generated successfully
111 // in order not to overwrite previous diagram in case of failure
112 auto path = std::filesystem::path{od} /
113 fmt::format("{}.{}", name, GeneratorTag::extension);
114 std::ofstream ofs;
115 ofs.open(path, std::ofstream::out | std::ofstream::trunc);
116 ofs << buffer.str();
117
118 ofs.close();
119
120 LOG_INFO("Written {} diagram to {}", name, path.string());
121 }
122 else {
123 LOG_INFO("Serialization to {} not supported for {}",
124 GeneratorTag::extension, name);
125 }
126}
127
128template <typename DiagramConfig>
129void generate_diagram_impl(const std::string &name,
130 std::shared_ptr<clanguml::config::diagram> diagram,
132 const std::vector<std::string> &translation_units,
133 const cli::runtime_config &runtime_config, std::function<void()> &&progress)
134{
135 using diagram_config = DiagramConfig;
136 using diagram_model = typename diagram_model_t<DiagramConfig>::type;
137 using diagram_visitor = typename diagram_visitor_t<DiagramConfig>::type;
138
139 auto model = clanguml::common::generators::generate<diagram_model,
140 diagram_config, diagram_visitor>(db, diagram->name,
141 dynamic_cast<diagram_config &>(*diagram), translation_units,
142 runtime_config.verbose, std::move(progress));
143
144 if constexpr (std::is_same_v<DiagramConfig, config::sequence_diagram>) {
145 if (runtime_config.print_from) {
146 auto from_values = model->list_from_values();
147
148 if (logging::logger_type() == logging::logger_type_t::text) {
149 for (const auto &from : from_values) {
150 std::cout << from << '\n';
151 }
152 }
153 else {
154 inja::json j = inja::json::array();
155 for (const auto &from : from_values) {
156 j.emplace_back(logging::escape_json(from));
157 }
158 std::cout << j.dump();
159 }
160
161 return;
162 }
163 if (runtime_config.print_to) {
164 auto to_values = model->list_to_values();
165 if (logging::logger_type() == logging::logger_type_t::text) {
166 for (const auto &to : to_values) {
167 std::cout << to << '\n';
168 }
169 }
170 else {
171 inja::json j = inja::json::array();
172 for (const auto &to : to_values) {
173 j.emplace_back(logging::escape_json(to));
174 }
175 std::cout << j.dump();
176 }
177 return;
178 }
179 }
180
181 for (const auto generator_type : runtime_config.generators) {
182 if (generator_type == generator_type_t::plantuml) {
185 runtime_config.output_directory, name, diagram, model);
186 }
187 else if (generator_type == generator_type_t::json) {
190 runtime_config.output_directory, name, diagram, model);
191 }
192 else if (generator_type == generator_type_t::mermaid) {
195 runtime_config.output_directory, name, diagram, model);
196 }
197 else if (generator_type == generator_type_t::graphml) {
200 runtime_config.output_directory, name, diagram, model);
201 }
202
203 // Convert plantuml or mermaid to an image using command provided
204 // in the command line arguments
205 if (runtime_config.render_diagrams) {
206 render_diagram(generator_type, diagram);
207 }
208 }
209}
210} // namespace detail
211
212void generate_diagram(const std::string &name,
213 std::shared_ptr<clanguml::config::diagram> diagram,
215 const std::vector<std::string> &translation_units,
216 const cli::runtime_config &runtime_config, std::function<void()> &&progress)
217{
220
225
226 if (diagram->type() == diagram_t::kClass) {
227 detail::generate_diagram_impl<class_diagram>(name, diagram, db,
228 translation_units, runtime_config, std::move(progress));
229 }
230 else if (diagram->type() == diagram_t::kSequence) {
231 detail::generate_diagram_impl<sequence_diagram>(name, diagram, db,
232 translation_units, runtime_config, std::move(progress));
233 }
234 else if (diagram->type() == diagram_t::kPackage) {
235 detail::generate_diagram_impl<package_diagram>(name, diagram, db,
236 translation_units, runtime_config, std::move(progress));
237 }
238 else if (diagram->type() == diagram_t::kInclude) {
239 detail::generate_diagram_impl<include_diagram>(name, diagram, db,
240 translation_units, runtime_config, std::move(progress));
241 }
242}
243
244int generate_diagrams(const std::vector<std::string> &diagram_names,
246 const cli::runtime_config &runtime_config,
247 const std::map<std::string, std::vector<std::string>>
248 &translation_units_map)
249{
250 util::thread_pool_executor generator_executor{runtime_config.thread_count};
251 std::vector<std::future<void>> futs;
252
253 std::unique_ptr<progress_indicator_base> indicator;
254
255 if (runtime_config.progress) {
256 if (clanguml::logging::logger_type() == logging::logger_type_t::text) {
257 std::cout
258 << termcolor::white
259 << "Processing translation units and generating diagrams:\n";
260 indicator = std::make_unique<progress_indicator>();
261 }
262 else {
263 indicator = std::make_unique<json_logger_progress_indicator>();
264 }
265 }
266
267 std::vector<std::exception_ptr> errors;
268
269 for (const auto &[name, diagram] : config.diagrams) {
270 // If there are any specific diagram names provided on the command
271 // line, and this diagram is not in that list - skip it
272 if (!diagram_names.empty() && !util::contains(diagram_names, name))
273 continue;
274
275 // If none of the generators supports the diagram type - skip it
276 bool at_least_one_generator_supports_diagram_type{false};
277 for (const auto generator_type : runtime_config.generators) {
278 if (generator_type == generator_type_t::plantuml) {
279 if (generator_supports_diagram_type<plantuml_generator_tag>(
280 diagram->type()))
281 at_least_one_generator_supports_diagram_type = true;
282 }
283 else if (generator_type == generator_type_t::json) {
284 if (generator_supports_diagram_type<json_generator_tag>(
285 diagram->type()))
286 at_least_one_generator_supports_diagram_type = true;
287 }
288 else if (generator_type == generator_type_t::mermaid) {
289 if (generator_supports_diagram_type<mermaid_generator_tag>(
290 diagram->type()))
291 at_least_one_generator_supports_diagram_type = true;
292 }
293 else if (generator_type == generator_type_t::graphml) {
294 if (generator_supports_diagram_type<graphml_generator_tag>(
295 diagram->type()))
296 at_least_one_generator_supports_diagram_type = true;
297 }
298 }
299
300 if (!at_least_one_generator_supports_diagram_type) {
301 LOG_INFO("Diagram '{}' not supported by any of selected "
302 "generators - skipping...",
303 name);
304 continue;
305 }
306
307 const auto &valid_translation_units = translation_units_map.at(name);
308
309 LOG_DBG("Found {} possible translation units for diagram '{}'",
310 valid_translation_units.size(), name);
311
312 const auto matching_commands_count =
313 db->count_matching_commands(valid_translation_units);
314
315 if (matching_commands_count == 0) {
316 const auto error_msg = fmt::format(
317 "Diagram '{}' generation failed: no translation units "
318 "found. Please make sure that your 'glob' patterns match "
319 "at least 1 file in 'compile_commands.json'.",
320 name);
321
322 if (indicator) {
323 indicator->add_progress_bar(
324 name, 0, diagram_type_to_color(diagram->type()));
325 indicator->fail(name);
326 try {
327 throw std::runtime_error(error_msg);
328 }
329 catch (std::runtime_error &e) {
330 errors.emplace_back(std::current_exception());
331 }
332 }
333 else {
334 LOG_ERROR(error_msg);
335 }
336
337 continue;
338 }
339
340 LOG_DBG("Found {} matching translation unit commands for diagram {}",
341 matching_commands_count, name);
342
343 auto generator = [&name = name, &diagram = diagram, &indicator,
344 db = std::ref(*db), matching_commands_count,
345 translation_units = valid_translation_units,
346 runtime_config]() mutable -> void {
347 try {
348 if (indicator) {
349 indicator->add_progress_bar(name, matching_commands_count,
350 diagram_type_to_color(diagram->type()));
351
352 generate_diagram(name, diagram, db, translation_units,
353 runtime_config, [&indicator, &name]() {
354 if (indicator)
355 indicator->increment(name);
356 });
357
358 if (indicator)
359 indicator->complete(name);
360 }
361 else {
362 generate_diagram(name, diagram, db, translation_units,
363 runtime_config, {});
364 }
365 }
367 if (indicator)
368 indicator->fail(name);
369 throw std::move(e);
370 }
371 catch (std::exception &e) {
372 if (indicator)
373 indicator->fail(name);
374
375 LOG_ERROR(
376 "Failed to generate diagram '{}': {}", name, e.what());
377
378 throw std::runtime_error(fmt::format(
379 "Failed to generate diagram '{}': {}", name, e.what()));
380 }
381 };
382
383 futs.emplace_back(generator_executor.add(std::move(generator)));
384 }
385
386 for (auto &fut : futs) {
387 try {
388 fut.get();
389 }
390 catch (std::exception &e) {
391 errors.emplace_back(std::current_exception());
392 }
393 }
394
395 if (runtime_config.progress &&
396 clanguml::logging::logger_type() == logging::logger_type_t::text) {
397 indicator->stop();
398 if (errors.empty()) {
399 std::cout << termcolor::white << "Done\n";
400 }
401 else {
402 std::cout << termcolor::white << "\n";
403 }
404 std::cout << termcolor::reset;
405 }
406
407 if (errors.empty())
408 return 0;
409
410 for (auto &e : errors) {
411 try {
412 std::rethrow_exception(e);
413 }
416 logging::logger_type_t::text) {
417
418 fmt::println("ERROR: Failed to generate {} diagram '{}' due to "
419 "following issues:",
420 e.diagram_type(), e.diagram_name());
421 for (const auto &d : e.diagnostics) {
422 fmt::println(" - {}", d);
423 }
424 fmt::println("");
425 }
426 else {
427 inja::json j;
428 j["diagram_name"] = e.diagram_name();
429 j["clang_errors"] = inja::json::array();
430 for (const auto &d : e.diagnostics) {
431 j["clang_errors"].emplace_back(d);
432 }
433
434 spdlog::get("clanguml-logger")
435 ->log(spdlog::level::err,
436 fmt::runtime(
437 R"("file": "{}", "line": {}, "message": {})"),
438 FILENAME_, __LINE__, j.dump());
439 }
440 }
441 catch (const std::exception &e) {
443 logging::logger_type_t::text) {
444 fmt::println("ERROR: {}", e.what());
445 }
446 else {
447 LOG_ERROR("{}", e.what());
448 }
449 }
450 }
451
452 return 1;
453}
454
455indicators::Color diagram_type_to_color(model::diagram_t diagram_type)
456{
457 switch (diagram_type) {
459 return indicators::Color::yellow;
461 return indicators::Color::blue;
463 return indicators::Color::cyan;
465 return indicators::Color::magenta;
466 default:
467 return indicators::Color::unspecified;
468 }
469}
470
471} // namespace clanguml::common::generators