0.6.3
C++ to UML diagram generator based on Clang
Loading...
Searching...
No Matches
clang_tool.cc
Go to the documentation of this file.
1/**
2 * @file src/common/generators/clang_tool.cc
3 *
4 * Copyright (c) 2021-2026 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 "clang_tool.h"
20
21#include <clang/Frontend/CompilerInstance.h>
22#include <clang/Frontend/CompilerInvocation.h>
23#include <clang/Tooling/CompilationDatabase.h>
24#if LLVM_VERSION_MAJOR >= 22
25#include <clang/Options/OptionUtils.h>
26#endif
27
28#include "util/util.h"
29
31
32namespace {
33void inject_resource_dir(
34 CommandLineArguments &args, const char *argv_0, void *main_addr)
35{
36 using namespace std::string_literals;
37
38 if (std::any_of(std::begin(args), std::end(args), [](const auto &arg) {
39 return util::starts_with(arg, "-resource-dir"s);
40 }))
41 return;
42
43 args = clang::tooling::getInsertArgumentAdjuster(("-resource-dir=" +
44#if LLVM_VERSION_MAJOR < 22
45 clang::CompilerInvocation::GetResourcesPath(argv_0, main_addr)
46#else
47 clang::GetResourcesPath(argv_0, main_addr)
48#endif
49 )
50 .c_str())(args, "");
51}
52} // namespace
53
54std::string to_string(const clanguml::generators::diagnostic &d)
55{
56 if (!d.location) {
57 return fmt::format("[{}] {}", d.level, d.description);
58 }
59
60 std::string filepath = d.location->file_relative().empty()
61 ? d.location->file()
62 : d.location->file_relative();
63 auto line = d.location->line();
64
65 return fmt::format(
66 "[{}] {}:{}: {}", d.level, filepath, line, d.description);
67}
68
69void to_json(nlohmann::json &j, const diagnostic &a)
70{
71 j["level"] = a.level;
72 j["description"] = logging::escape_json(a.description);
73 if (a.location) {
74 j["location"]["file"] = a.location.value().file();
75 j["location"]["line"] = a.location.value().line();
76 j["location"]["column"] = a.location.value().column();
77 }
78}
79
81 std::string dn, std::vector<diagnostic> d, std::string description)
82 : error::diagram_generation_error{dt, dn, description}
83 , diagnostics{std::move(d)}
84{
85}
86
87diagnostic_consumer::diagnostic_consumer(std::filesystem::path relative_to)
88 : relative_to_{std::move(relative_to)}
89{
90}
91
93 DiagnosticsEngine::Level diag_level, const Diagnostic &info)
94{
95 SmallVector<char> buf{};
96 info.FormatDiagnostic(buf);
97
98 diagnostic d;
99 d.level = diag_level;
100 d.description = std::string{buf.data(), buf.size()};
101
102 if (info.hasSourceManager() && info.getLocation().isValid()) {
104 common::set_source_location(info.getSourceManager(), info.getLocation(),
105 *d.location, {}, relative_to_);
106 }
107
108 if (diag_level == clang::DiagnosticsEngine::Level::Error ||
109 diag_level == clang::DiagnosticsEngine::Level::Fatal) {
110 failed = true;
111 }
112
113 diagnostics.emplace_back(std::move(d));
114}
115
117 std::string diagram_name,
118 const clanguml::common::compilation_database &compilation_database,
119 const std::vector<std::string> &source_paths,
120 std::filesystem::path relative_to, bool quiet)
121 : diagram_type_{diagram_type}
122 , diagram_name_{std::move(diagram_name)}
123 , compilations_{compilation_database}
124 , source_paths_{source_paths}
125 , quiet_{quiet}
126 , pch_container_ops_{std::make_shared<PCHContainerOperations>()}
127 , overlay_fs_{new llvm::vfs::OverlayFileSystem(
128 llvm::vfs::getRealFileSystem())}
129 , inmemory_fs_{new llvm::vfs::InMemoryFileSystem}
130 , files_{new FileManager(FileSystemOptions(), overlay_fs_)}
131 , diag_consumer_{std::make_unique<diagnostic_consumer>(relative_to)}
132 , diag_opts_{new clang::DiagnosticOptions}
133{
134 overlay_fs_->pushOverlay(inmemory_fs_);
135
136 append_arguments_adjuster(getClangStripOutputAdjuster());
137 append_arguments_adjuster(getClangSyntaxOnlyAdjuster());
138 append_arguments_adjuster(getClangStripDependencyFileAdjuster());
139}
140
141clang_tool::~clang_tool() = default;
142
143void clang_tool::append_arguments_adjuster(ArgumentsAdjuster Adjuster)
144{
146 combineAdjusters(std::move(args_adjuster_), std::move(Adjuster));
147}
148
149void clang_tool::run(ToolAction *Action)
150{
151 static int static_symbol;
152
153 std::vector<std::string> absolute_tu_paths;
154 absolute_tu_paths.reserve(source_paths_.size());
155 for (const auto &source_path : source_paths_) {
156 auto absolute_tu_path =
157 clang::tooling::getAbsolutePath(*overlay_fs_, source_path);
158 if (!absolute_tu_path) {
159 if (!quiet_) {
160 LOG_WARN("Skipping file {} in diagram {}. Could not resolve "
161 "absolute path for translation unit: {}",
162 source_path, diagram_name_,
163 llvm::toString(absolute_tu_path.takeError()));
164 }
165 continue;
166 }
167 absolute_tu_paths.emplace_back(std::move(*absolute_tu_path));
168 }
169
170 // Remember the working directory in case we need to restore it.
171 std::string initial_workdir;
172 if (auto current_workdir = overlay_fs_->getCurrentWorkingDirectory()) {
173 initial_workdir = std::move(*current_workdir);
174 }
175 else {
176 if (!quiet_)
177 LOG_ERROR("Could not get current working directory when generating "
178 "diagram '{}': {}",
179 diagram_name_, current_workdir.getError().message());
180 }
181
182 for (const auto &file : absolute_tu_paths) {
183 if (!quiet_)
184 LOG_INFO("Processing diagram '{}' translation unit: {}",
185 diagram_name_, file);
186
187 auto compile_commands_for_file = compilations_.getCompileCommands(file);
188
189 if (compile_commands_for_file.empty()) {
190 if (!quiet_)
191 LOG_WARN(
192 "Skipping file {} for diagram '{}'. Compilation command "
193 "not found.",
194 file, diagram_name_);
195 continue;
196 }
197
198 if (compile_commands_for_file.size() > 1 &&
200 LOG_WARN("Multiple compile commands detected for file '{}' in "
201 "diagram '{}' - using only the first one...",
202 file, diagram_name_);
203 }
204
205 for (auto &compile_command : compile_commands_for_file) {
206 if (overlay_fs_->setCurrentWorkingDirectory(
207 compile_command.Directory))
208 llvm::report_fatal_error("Cannot chdir into \"" +
209 Twine(compile_command.Directory) + "\"!");
210
211 // Now fill the in-memory VFS with the relative file mappings so it
212 // will have the correct relative paths. We never remove mappings
213 // but that should be fine.
214 if (visited_working_directories_.insert(compile_command.Directory)
215 .second) {
216 for (const auto &[file_name, file_content] :
218 if (!llvm::sys::path::is_absolute(file_name))
219 inmemory_fs_->addFile(file_name, 0,
220 llvm::MemoryBuffer::getMemBuffer(file_content));
221 }
222
223 auto command_line = compile_command.CommandLine;
224 if (args_adjuster_)
225 command_line =
226 args_adjuster_(command_line, compile_command.Filename);
227
228 assert(!command_line.empty());
229
230 inject_resource_dir(command_line, "clang_tool", &static_symbol);
231
232 ToolInvocation invocation(std::move(command_line), Action,
234 invocation.setDiagnosticConsumer(diag_consumer_.get());
235#if LLVM_VERSION_MAJOR > 13
236 invocation.setDiagnosticOptions(diag_opts_.get());
237#endif
238
239 if (!invocation.run() || diag_consumer_->failed) {
240 if (!initial_workdir.empty()) {
241 if (const auto ec = overlay_fs_->setCurrentWorkingDirectory(
242 initial_workdir);
243 ec)
244 if (!quiet_)
245 LOG_ERROR("Error when trying to restore working "
246 "directory: {}",
247 ec.message());
248 }
249
250 if (diag_consumer_ && diag_consumer_->failed) {
251 if (!(diag_consumer_->diagnostics.empty())) {
253 diag_consumer_->diagnostics,
254 to_string(diag_consumer_->diagnostics.back()));
255 }
256
258 diag_consumer_->diagnostics);
259 }
260
261 throw std::runtime_error(
262 fmt::format("Unknown error while processing {}", file));
263 }
264
266 break;
267 }
268 }
269
270 if (!initial_workdir.empty()) {
271 if (const auto ec =
272 overlay_fs_->setCurrentWorkingDirectory(initial_workdir))
273 if (!quiet_)
274 LOG_ERROR("Error when trying to restore working dir: {}",
275 ec.message());
276 }
277}
278} // namespace clanguml::generators
279
280namespace clang {
281std::string to_string(clang::DiagnosticsEngine::Level level)
282{
283 std::string level_str;
284 switch (level) {
285 case clang::DiagnosticsEngine::Ignored:
286 level_str = "IGNORED";
287 break;
288 case clang::DiagnosticsEngine::Note:
289 level_str = "NOTE";
290 break;
291 case clang::DiagnosticsEngine::Remark:
292 level_str = "REMARK";
293 break;
294 case clang::DiagnosticsEngine::Warning:
295 level_str = "WARNING";
296 break;
297 case clang::DiagnosticsEngine::Error:
298 level_str = "ERROR";
299 break;
300 case clang::DiagnosticsEngine::Fatal:
301 level_str = "FATAL";
302 break;
303 default:
304 level_str = "UNKNOWN";
305 break;
306 }
307
308 return level_str;
309}
310} // namespace clang