Coverage Report

Created: 2026-07-30 06:52

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/CMake/Source/cmCMakeSarifLogger.cxx
Line
Count
Source
1
/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
2
   file LICENSE.rst or https://cmake.org/licensing for details.  */
3
#include "cmCMakeSarifLogger.h"
4
5
#include <cstddef>
6
#include <limits>
7
#include <unordered_map>
8
#include <utility>
9
#include <vector>
10
11
#include <cm/string_view>
12
13
#include "cmsys/FStream.hxx"
14
#include "cmsys/String.h"
15
16
#include "cmDiagnostics.h"
17
#include "cmListFileCache.h"
18
#include "cmMessageType.h"
19
#include "cmMessenger.h"
20
#include "cmSarif.h"
21
#include "cmState.h"
22
#include "cmStringAlgorithms.h"
23
#include "cmSystemTools.h"
24
#include "cmValue.h"
25
#include "cmVersionConfig.h"
26
#include "cmake.h"
27
28
// CMake-specific SARIF helpers
29
namespace {
30
31
constexpr char const* CMakeSarifOutputFlag = "CMAKE_EXPORT_SARIF";
32
constexpr char const* DefaultSarifFile = ".cmake/sarif/cmake.sarif";
33
34
/// @brief Express the location of a `cmListFileContext` in SARIF
35
/// @param[in] uriBaseIds A list of logical base directory names and their path
36
///
37
/// Build a SARIF location object detailing the location data available from a
38
/// context. More specific information like the region (line number) and
39
/// function call name will be included if available.
40
///
41
/// SARIF requests that paths are given relative to a logical base for
42
/// relocatability. Context locations will be made relative to a logical base
43
/// iff they fall under one of the directories listed in the `uriBaseIds`
44
/// map. Bases are tried in order.
45
cmSarif::Location LocationFromContext(
46
  cmListFileContext const& lfc,
47
  std::vector<std::pair<cm::string_view, cm::string_view>> const&
48
    uriBaseIds = {})
49
0
{
50
0
  cmSarif::Location location;
51
0
  location.Physical.Artifact.Uri = lfc.FilePath;
52
53
  // SARIF requests that paths are given relative to a logical base for
54
  // relocatability. Check if these files are under any of the bases, if
55
  // provided.
56
0
  for (auto const& baseUri : uriBaseIds) {
57
0
    std::string relative = cmSystemTools::RelativeIfUnder(
58
0
      std::string(baseUri.second), location.Physical.Artifact.Uri);
59
0
    if (relative != location.Physical.Artifact.Uri) {
60
0
      location.Physical.Artifact.Uri = relative;
61
0
      location.Physical.Artifact.UriBaseId = std::string(baseUri.first);
62
0
    }
63
0
  }
64
65
0
  if (!lfc.Name.empty()) {
66
0
    location.Logical.emplace_back(
67
0
      cmSarif::LogicalLocation{ lfc.Name, cmSarif::LocationKind::Function });
68
0
  }
69
70
  // Add info about the region within the file depending on how specific the
71
  // context is. Watch for deferred call and variable watch placeholders or
72
  // a zero, which indicates the start of processing a list file.
73
0
  if (lfc.Line == cmListFileContext::DeferPlaceholderLine) {
74
0
    location.Message = cmSarif::Message{ "DEFERRED" };
75
0
  } else if (lfc.Line > 0 && lfc.Line != std::numeric_limits<long>::max()) {
76
0
    cmSarif::Region region;
77
0
    region.StartLine = lfc.Line;
78
0
    location.Physical.ArtifactRegion = region;
79
0
  }
80
81
0
  return location;
82
0
}
83
84
cm::optional<cmSarif::Location> LastLocation(
85
  cmListFileBacktrace backtrace,
86
  std::vector<std::pair<cm::string_view, cm::string_view>> const&
87
    uriBaseIds = {})
88
0
{
89
0
  if (backtrace.Empty()) {
90
0
    return {};
91
0
  }
92
0
  return LocationFromContext(backtrace.Top(), uriBaseIds);
93
0
}
94
95
cm::optional<cmSarif::Stack> StackFromBacktrace(
96
  cmListFileBacktrace bt,
97
  std::vector<std::pair<cm::string_view, cm::string_view>> const&
98
    uriBaseIds = {})
99
0
{
100
0
  if (bt.Empty()) {
101
0
    return {};
102
0
  }
103
104
0
  cmSarif::Stack stack;
105
0
  for (; !bt.Empty(); bt = bt.Pop()) {
106
0
    cmSarif::Location topLocation = LocationFromContext(bt.Top(), uriBaseIds);
107
108
    // If the location doesn't have a specific region, this entry is a
109
    // placeholder and should not appear in the call stack.
110
0
    if (!topLocation.Message && !topLocation.Physical.ArtifactRegion) {
111
0
      continue;
112
0
    }
113
114
0
    cmSarif::StackFrame frame;
115
0
    frame.Location = std::move(topLocation);
116
0
    stack.Frames.emplace_back(std::move(frame));
117
0
  }
118
0
  return stack;
119
0
}
120
121
cmSarif::Tool CreateCMakeTool()
122
0
{
123
0
  cmSarif::ToolComponent cmDriver;
124
0
  cmDriver.Name = "CMake";
125
0
  cmDriver.Version = CMake_VERSION;
126
127
0
  return cmSarif::Tool{ cmDriver };
128
0
}
129
130
std::string RuleIdForMessageType(MessageType type,
131
                                 cmDiagnosticCategory category)
132
0
{
133
0
  cm::string_view name = cmDiagnostics::GetCategoryString(category);
134
0
  if (!name.empty()) {
135
    // Strip the "CMD_" prefix from the category name and convert to PascalCase
136
0
    std::string sarifIdName;
137
0
    bool nextWord = true;
138
0
    for (char c : name.substr(4)) {
139
0
      if (c == '_') {
140
0
        nextWord = true;
141
0
        continue;
142
0
      }
143
0
      if (nextWord) {
144
0
        sarifIdName += c;
145
0
        nextWord = false;
146
0
      } else {
147
0
        sarifIdName += cmsysString_tolower(c);
148
0
      }
149
0
    }
150
0
    return cmStrCat("CMake.", sarifIdName);
151
0
  }
152
153
  // Fall back to message type if not a diagnostic
154
0
  switch (type) {
155
0
    case MessageType::FATAL_ERROR:
156
0
      return "CMake.FatalError";
157
0
    case MessageType::INTERNAL_ERROR:
158
0
      return "CMake.InternalError";
159
0
    case MessageType::WARNING:
160
0
      return "CMake.Warning";
161
0
    default:
162
0
      return "";
163
0
  }
164
0
}
165
166
cm::string_view NameForMessageType(MessageType type,
167
                                   cmDiagnosticCategory category)
168
0
{
169
0
  if (category != cmDiagnostics::CMD_NONE) {
170
0
    return cmDiagnostics::GetCategoryString(category);
171
0
  }
172
0
  switch (type) {
173
0
    case MessageType::FATAL_ERROR:
174
0
      return "CMake Error";
175
0
    case MessageType::INTERNAL_ERROR:
176
0
      return "CMake Internal Error";
177
0
    case MessageType::WARNING:
178
0
      return "CMake Warning";
179
0
    default:
180
0
      return "";
181
0
  }
182
0
}
183
184
cmSarif::ReportingDescriptor ReportingDescriptorForMessageType(
185
  MessageType type, cmDiagnosticCategory category)
186
0
{
187
0
  cmSarif::ReportingDescriptor rd;
188
0
  rd.Id = RuleIdForMessageType(type, category);
189
0
  rd.Name = NameForMessageType(type, category);
190
0
  return rd;
191
0
};
192
193
cmSarif::ResultSeverityLevel SarifLevelFromMessageType(MessageType type)
194
0
{
195
0
  switch (type) {
196
0
    case MessageType::FATAL_ERROR:
197
0
    case MessageType::INTERNAL_ERROR:
198
0
      return cmSarif::ResultSeverityLevel::Error;
199
0
    case MessageType::WARNING:
200
0
      return cmSarif::ResultSeverityLevel::Warning;
201
0
    default:
202
0
      return cmSarif::ResultSeverityLevel::Note;
203
0
  }
204
0
}
205
206
} // namespace
207
208
cmCMakeSarifLogger::cmCMakeSarifLogger(cmake& cm)
209
1
  : CM(cm)
210
1
{
211
1
  if (this->CM.GetState()->GetRole() == cmState::Role::Project) {
212
0
    cm.MarkCliAsUsed(CMakeSarifOutputFlag);
213
0
  }
214
1
}
215
216
cmCMakeSarifLogger::~cmCMakeSarifLogger()
217
1
{
218
1
  this->GenerateForRun();
219
1
}
220
221
cm::optional<std::string> cmCMakeSarifLogger::FileOutputPath() const
222
1
{
223
  // If a SARIF path was specified via CLI, use it. Otherwise, check whether
224
  // logging is enabled via the project cache variable and use the default
225
  // path if so.
226
1
  if (cm::optional<std::string> specifiedPath = this->CM.GetSarifFilePath()) {
227
0
    return specifiedPath;
228
0
  }
229
1
  if (this->CM.GetState()->GetRole() == cmState::Role::Project &&
230
0
      this->CM.GetCacheDefinition(CMakeSarifOutputFlag).IsOn()) {
231
0
    return cmStrCat(this->CM.GetHomeOutputDirectory(), '/', DefaultSarifFile);
232
0
  }
233
1
  return cm::nullopt;
234
1
}
235
236
bool cmCMakeSarifLogger::WriteFile(std::string const& path,
237
                                   bool createParentDirectories) const
238
0
{
239
0
  if (createParentDirectories) {
240
0
    if (!cmSystemTools::MakeDirectory(cmSystemTools::GetFilenamePath(path))
241
0
           .IsSuccess()) {
242
0
      return false;
243
0
    }
244
0
  }
245
246
0
  cmsys::ofstream outputFile(path);
247
0
  if (!outputFile.good()) {
248
0
    return false;
249
0
  }
250
251
  // Run object to build
252
0
  cmSarif::Run run;
253
0
  run.Tool = CreateCMakeTool();
254
255
  // Helper to add rules to the run as encountered in results and get their
256
  // index for reporting
257
0
  std::unordered_map<std::string, std::size_t> ruleIndices;
258
0
  auto use_rule = [&](MessageType type, cmDiagnosticCategory category) {
259
0
    std::string category_name = RuleIdForMessageType(type, category);
260
0
    auto result = ruleIndices.emplace(category_name, 0);
261
0
    if (result.second) {
262
0
      result.first->second = run.Tool.Driver.Rules.size();
263
0
      run.Tool.Driver.Rules.emplace_back(
264
0
        ReportingDescriptorForMessageType(type, category));
265
0
    }
266
0
    return *result.first;
267
0
  };
268
269
  // Make a prioritized list of base directories applicable in this context.
270
  // This is used for normalizing the paths of related locations.
271
0
  std::vector<std::pair<cm::string_view, cm::string_view>> uriBaseIds;
272
273
0
  std::string const& binDir = this->CM.GetHomeOutputDirectory();
274
0
  if (!binDir.empty()) {
275
0
    uriBaseIds.emplace_back("CMAKE_BINARY_DIR", binDir);
276
0
  }
277
278
0
  std::string const& homeDir = this->CM.GetHomeDirectory();
279
0
  if (!homeDir.empty()) {
280
0
    uriBaseIds.emplace_back("CMAKE_SOURCE_DIR", homeDir);
281
0
  }
282
283
  // Log the base directories for this run.
284
0
  for (auto const& base : uriBaseIds) {
285
0
    run.OriginalUriBaseIds.emplace(
286
0
      std::string(base.first),
287
0
      cmSarif::ArtifactLocation{ cmStrCat("file://", base.second, "/"), "" });
288
0
  }
289
290
0
  cmMessenger const& messenger = *this->CM.GetMessenger();
291
0
  for (auto const& message : messenger.GetDisplayedMessages()) {
292
    // SARIF should only emit diagnostic messages, not general messages/logs
293
0
    switch (message.Type) {
294
0
      case MessageType::MESSAGE:
295
0
      case MessageType::LOG:
296
0
      case MessageType::UNDEFINED:
297
0
        continue;
298
0
      default:
299
0
        break;
300
0
    }
301
302
0
    std::pair<std::string, std::size_t> ruleInfo =
303
0
      use_rule(message.Type, message.Category);
304
305
0
    cmSarif::Result result;
306
0
    result.RuleId = ruleInfo.first;
307
0
    result.RuleIndex = ruleInfo.second;
308
0
    result.Message = cmSarif::Message{ message.Text };
309
0
    result.Location = LastLocation(message.Backtrace, uriBaseIds);
310
0
    if (cm::optional<cmSarif::Stack> stack =
311
0
          StackFromBacktrace(message.Backtrace, uriBaseIds)) {
312
0
      result.Stacks.emplace_back(std::move(*stack));
313
0
    }
314
0
    result.Level = SarifLevelFromMessageType(message.Type);
315
316
0
    run.Results.emplace_back(std::move(result));
317
0
  }
318
319
0
  return cmSarif::WriteLog(path, run);
320
0
}
321
322
void cmCMakeSarifLogger::GenerateForRun() const
323
1
{
324
1
  cm::optional<std::string> path = this->FileOutputPath();
325
1
  if (!path) {
326
1
    return;
327
1
  }
328
329
  // If using the default path within the build dir, ensure parents are created
330
0
  bool const createParents = !this->CM.GetSarifFilePath().has_value();
331
0
  if (!this->WriteFile(*path, createParents)) {
332
0
    cmSystemTools::Error(cmStrCat("Failed to write SARIF log to ", *path));
333
0
  }
334
0
}