Coverage Report

Created: 2026-03-12 06:35

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/CMake/Source/cmSarifLog.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 "cmSarifLog.h"
4
5
#include <memory>
6
#include <stdexcept>
7
8
#include <cm3p/json/value.h>
9
#include <cm3p/json/writer.h>
10
11
#include "cmsys/FStream.hxx"
12
13
#include "cmListFileCache.h"
14
#include "cmMessageType.h"
15
#include "cmState.h"
16
#include "cmStringAlgorithms.h"
17
#include "cmSystemTools.h"
18
#include "cmValue.h"
19
#include "cmVersionConfig.h"
20
#include "cmake.h"
21
22
cmSarif::ResultsLog::ResultsLog()
23
35
{
24
  // Add the known CMake rules
25
35
  this->KnownRules.emplace(RuleBuilder("CMake.AuthorWarning")
26
35
                             .Name("CMake Warning (dev)")
27
35
                             .DefaultMessage("CMake Warning (dev): {0}")
28
35
                             .Build());
29
35
  this->KnownRules.emplace(RuleBuilder("CMake.Warning")
30
35
                             .Name("CMake Warning")
31
35
                             .DefaultMessage("CMake Warning: {0}")
32
35
                             .Build());
33
35
  this->KnownRules.emplace(RuleBuilder("CMake.DeprecationWarning")
34
35
                             .Name("CMake Deprecation Warning")
35
35
                             .DefaultMessage("CMake Deprecation Warning: {0}")
36
35
                             .Build());
37
35
  this->KnownRules.emplace(RuleBuilder("CMake.AuthorError")
38
35
                             .Name("CMake Error (dev)")
39
35
                             .DefaultMessage("CMake Error (dev): {0}")
40
35
                             .Build());
41
35
  this->KnownRules.emplace(RuleBuilder("CMake.FatalError")
42
35
                             .Name("CMake Error")
43
35
                             .DefaultMessage("CMake Error: {0}")
44
35
                             .Build());
45
35
  this->KnownRules.emplace(
46
35
    RuleBuilder("CMake.InternalError")
47
35
      .Name("CMake Internal Error")
48
35
      .DefaultMessage("CMake Internal Error (please report a bug): {0}")
49
35
      .Build());
50
35
  this->KnownRules.emplace(RuleBuilder("CMake.DeprecationError")
51
35
                             .Name("CMake Deprecation Error")
52
35
                             .DefaultMessage("CMake Deprecation Error: {0}")
53
35
                             .Build());
54
35
  this->KnownRules.emplace(RuleBuilder("CMake.Message")
55
35
                             .Name("CMake Message")
56
35
                             .DefaultMessage("CMake Message: {0}")
57
35
                             .Build());
58
35
  this->KnownRules.emplace(RuleBuilder("CMake.Log")
59
35
                             .Name("CMake Log")
60
35
                             .DefaultMessage("CMake Log: {0}")
61
35
                             .Build());
62
35
}
63
64
void cmSarif::ResultsLog::Log(cmSarif::Result&& result) const
65
1
{
66
  // The rule ID is optional, but if it is present, enable metadata output for
67
  // the rule by marking it as used
68
1
  if (result.RuleId) {
69
1
    std::size_t index = this->UseRule(*result.RuleId);
70
1
    result.RuleIndex = index;
71
1
  }
72
73
  // Add the result to the log
74
1
  this->Results.emplace_back(result);
75
1
}
76
77
void cmSarif::ResultsLog::LogMessage(
78
  MessageType t, std::string const& text,
79
  cmListFileBacktrace const& backtrace) const
80
1
{
81
  // Add metadata to the result object
82
  // The CMake SARIF rules for messages all expect 1 string argument with the
83
  // message text
84
1
  Json::Value additionalProperties(Json::objectValue);
85
1
  Json::Value args(Json::arrayValue);
86
1
  args.append(text);
87
1
  additionalProperties["message"]["id"] = "default";
88
1
  additionalProperties["message"]["arguments"] = args;
89
90
  // Create and log a result object
91
  // Rule indices are assigned when writing the final JSON output. Right now,
92
  // leave it as nullopt. The other optional fields are filled if available
93
1
  this->Log(cmSarif::Result{
94
1
    text, cmSarif::SourceFileLocation::FromBacktrace(backtrace),
95
1
    cmSarif::MessageSeverityLevel(t), cmSarif::MessageRuleId(t), cm::nullopt,
96
1
    additionalProperties });
97
1
}
98
99
std::size_t cmSarif::ResultsLog::UseRule(std::string const& id) const
100
1
{
101
  // Check if the rule is already in the index
102
1
  auto it = this->RuleToIndex.find(id);
103
1
  if (it != this->RuleToIndex.end()) {
104
    // The rule is already in use. Return the known index
105
0
    return it->second;
106
0
  }
107
108
  // This rule is not yet in the index, so check if it is recognized
109
1
  auto itKnown = this->KnownRules.find(id);
110
1
  if (itKnown == this->KnownRules.end()) {
111
    // The rule is not known. Add an empty rule to the known rules so that it
112
    // is included in the output
113
0
    this->KnownRules.emplace(RuleBuilder(id.c_str()).Build());
114
0
  }
115
116
  // Since this is the first time the rule is used, enable it and add it to the
117
  // index
118
1
  std::size_t idx = this->EnabledRules.size();
119
1
  this->RuleToIndex[id] = idx;
120
1
  this->EnabledRules.emplace_back(id);
121
1
  return idx;
122
1
}
123
124
cmSarif::ResultSeverityLevel cmSarif::MessageSeverityLevel(MessageType t)
125
1
{
126
1
  switch (t) {
127
0
    case MessageType::AUTHOR_WARNING:
128
0
    case MessageType::WARNING:
129
0
    case MessageType::DEPRECATION_WARNING:
130
0
      return ResultSeverityLevel::SARIF_WARNING;
131
0
    case MessageType::AUTHOR_ERROR:
132
1
    case MessageType::FATAL_ERROR:
133
1
    case MessageType::INTERNAL_ERROR:
134
1
    case MessageType::DEPRECATION_ERROR:
135
1
      return ResultSeverityLevel::SARIF_ERROR;
136
0
    case MessageType::MESSAGE:
137
0
    case MessageType::LOG:
138
0
      return ResultSeverityLevel::SARIF_NOTE;
139
0
    default:
140
0
      return ResultSeverityLevel::SARIF_NONE;
141
1
  }
142
1
}
143
144
cm::optional<std::string> cmSarif::MessageRuleId(MessageType t)
145
1
{
146
1
  switch (t) {
147
0
    case MessageType::AUTHOR_WARNING:
148
0
      return "CMake.AuthorWarning";
149
0
    case MessageType::WARNING:
150
0
      return "CMake.Warning";
151
0
    case MessageType::DEPRECATION_WARNING:
152
0
      return "CMake.DeprecationWarning";
153
0
    case MessageType::AUTHOR_ERROR:
154
0
      return "CMake.AuthorError";
155
1
    case MessageType::FATAL_ERROR:
156
1
      return "CMake.FatalError";
157
0
    case MessageType::INTERNAL_ERROR:
158
0
      return "CMake.InternalError";
159
0
    case MessageType::DEPRECATION_ERROR:
160
0
      return "CMake.DeprecationError";
161
0
    case MessageType::MESSAGE:
162
0
      return "CMake.Message";
163
0
    case MessageType::LOG:
164
0
      return "CMake.Log";
165
0
    default:
166
0
      return cm::nullopt;
167
1
  }
168
1
}
169
170
Json::Value cmSarif::Rule::GetJson() const
171
0
{
172
0
  Json::Value rule(Json::objectValue);
173
0
  rule["id"] = this->Id;
174
175
0
  if (this->Name) {
176
0
    rule["name"] = *this->Name;
177
0
  }
178
0
  if (this->FullDescription) {
179
0
    rule["fullDescription"]["text"] = *this->FullDescription;
180
0
  }
181
0
  if (this->DefaultMessage) {
182
0
    rule["messageStrings"]["default"]["text"] = *this->DefaultMessage;
183
0
  }
184
185
0
  return rule;
186
0
}
187
188
cmSarif::SourceFileLocation::SourceFileLocation(
189
  cmListFileBacktrace const& backtrace)
190
1
{
191
1
  if (backtrace.Empty()) {
192
0
    throw std::runtime_error("Empty source file location");
193
0
  }
194
195
1
  cmListFileContext const& lfc = backtrace.Top();
196
1
  this->Uri = lfc.FilePath;
197
1
  this->Line = lfc.Line;
198
1
}
199
200
cm::optional<cmSarif::SourceFileLocation>
201
cmSarif::SourceFileLocation::FromBacktrace(
202
  cmListFileBacktrace const& backtrace)
203
1
{
204
1
  if (backtrace.Empty()) {
205
0
    return cm::nullopt;
206
0
  }
207
1
  cmListFileContext const& lfc = backtrace.Top();
208
1
  if (lfc.Line <= 0 || lfc.FilePath.empty()) {
209
0
    return cm::nullopt;
210
0
  }
211
212
1
  return cm::make_optional<cmSarif::SourceFileLocation>(backtrace);
213
1
}
214
215
void cmSarif::ResultsLog::WriteJson(Json::Value& root) const
216
0
{
217
  // Add SARIF metadata
218
0
  root["version"] = "2.1.0";
219
0
  root["$schema"] = "https://schemastore.azurewebsites.net/schemas/json/"
220
0
                    "sarif-2.1.0-rtm.4.json";
221
222
  // JSON object for the SARIF runs array
223
0
  Json::Value runs(Json::arrayValue);
224
225
  // JSON object for the current (only) run
226
0
  Json::Value currentRun(Json::objectValue);
227
228
  // Accumulate info about the reported rules
229
0
  Json::Value jsonRules(Json::arrayValue);
230
0
  for (auto const& ruleId : this->EnabledRules) {
231
0
    jsonRules.append(KnownRules.at(ruleId).GetJson());
232
0
  }
233
234
  // Add info the driver for the current run (CMake)
235
0
  Json::Value driverTool(Json::objectValue);
236
0
  driverTool["name"] = "CMake";
237
0
  driverTool["version"] = CMake_VERSION;
238
0
  driverTool["rules"] = jsonRules;
239
0
  currentRun["tool"]["driver"] = driverTool;
240
241
0
  runs.append(currentRun);
242
243
  // Add all results
244
0
  Json::Value jsonResults(Json::arrayValue);
245
0
  for (auto const& res : this->Results) {
246
0
    Json::Value jsonResult(Json::objectValue);
247
248
0
    if (res.Message) {
249
0
      jsonResult["message"]["text"] = *(res.Message);
250
0
    }
251
252
    // If the result has a level, add it to the result
253
0
    if (res.Level) {
254
0
      switch (*res.Level) {
255
0
        case ResultSeverityLevel::SARIF_WARNING:
256
0
          jsonResult["level"] = "warning";
257
0
          break;
258
0
        case ResultSeverityLevel::SARIF_ERROR:
259
0
          jsonResult["level"] = "error";
260
0
          break;
261
0
        case ResultSeverityLevel::SARIF_NOTE:
262
0
          jsonResult["level"] = "note";
263
0
          break;
264
0
        case ResultSeverityLevel::SARIF_NONE:
265
0
          jsonResult["level"] = "none";
266
0
          break;
267
0
      }
268
0
    }
269
270
    // If the result has a rule ID or index, add it to the result
271
0
    if (res.RuleId) {
272
0
      jsonResult["ruleId"] = *res.RuleId;
273
0
    }
274
0
    if (res.RuleIndex) {
275
0
      jsonResult["ruleIndex"] = Json::UInt64(*res.RuleIndex);
276
0
    }
277
278
0
    if (res.Location) {
279
0
      jsonResult["locations"][0]["physicalLocation"]["artifactLocation"]
280
0
                ["uri"] = (res.Location)->Uri;
281
0
      jsonResult["locations"][0]["physicalLocation"]["region"]["startLine"] =
282
0
        Json::Int64((res.Location)->Line);
283
0
    }
284
285
0
    jsonResults.append(jsonResult);
286
0
  }
287
288
0
  currentRun["results"] = jsonResults;
289
0
  runs[0] = currentRun;
290
0
  root["runs"] = runs;
291
0
}
292
293
cmSarif::LogFileWriter::~LogFileWriter()
294
1
{
295
  // If the file has not been written yet, try to finalize it
296
1
  if (!this->FileWritten) {
297
    // Try to write and check the result
298
1
    if (this->TryWrite() == WriteResult::FAILURE) {
299
      // If the result is `FAILURE`, it means the write condition is true but
300
      // the file still wasn't written. This is an error.
301
0
      cmSystemTools::Error("Failed to write SARIF log to " + this->FilePath);
302
0
    }
303
1
  }
304
1
}
305
306
bool cmSarif::LogFileWriter::EnsureFileValid()
307
0
{
308
  // First, ensure directory exists
309
0
  std::string const dir = cmSystemTools::GetFilenamePath(this->FilePath);
310
0
  if (!cmSystemTools::FileIsDirectory(dir)) {
311
0
    if (!this->CreateDirectories ||
312
0
        !cmSystemTools::MakeDirectory(dir).IsSuccess()) {
313
0
      return false;
314
0
    }
315
0
  }
316
317
  // Open the file for writing
318
0
  cmsys::ofstream outputFile(this->FilePath.c_str());
319
0
  if (!outputFile.good()) {
320
0
    return false;
321
0
  }
322
0
  return true;
323
0
}
324
325
cmSarif::LogFileWriter::WriteResult cmSarif::LogFileWriter::TryWrite()
326
1
{
327
  // Check that SARIF logging is enabled
328
1
  if (!this->WriteCondition || !this->WriteCondition()) {
329
1
    return WriteResult::SKIPPED;
330
1
  }
331
332
  // Open the file
333
0
  if (!this->EnsureFileValid()) {
334
0
    return WriteResult::FAILURE;
335
0
  }
336
0
  cmsys::ofstream outputFile(this->FilePath.c_str());
337
338
  // The file is available, so proceed to write the log
339
340
  // Assemble the SARIF JSON from the results in the log
341
0
  Json::Value root(Json::objectValue);
342
0
  this->Log.WriteJson(root);
343
344
  // Serialize the JSON to the file
345
0
  Json::StreamWriterBuilder builder;
346
0
  std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
347
348
0
  writer->write(root, &outputFile);
349
0
  outputFile.close();
350
351
0
  this->FileWritten = true;
352
0
  return WriteResult::SUCCESS;
353
0
}
354
355
bool cmSarif::LogFileWriter::ConfigureForCMakeRun(cmake& cm)
356
1
{
357
  // If an explicit SARIF output path has been provided, set and check it
358
1
  if (cm::optional<std::string> sarifFilePath = cm.GetSarifFilePath()) {
359
0
    this->SetPath(*sarifFilePath);
360
0
    if (!this->EnsureFileValid()) {
361
0
      cmSystemTools::Error(
362
0
        cmStrCat("Invalid SARIF output file path: ", *sarifFilePath));
363
0
      return false;
364
0
    }
365
0
  }
366
367
  // The write condition is checked immediately before writing the file, which
368
  // allows projects to enable SARIF diagnostics by setting a cache variable
369
  // and have it take effect for the current run.
370
1
  this->SetWriteCondition([&cm]() {
371
    // The command-line option can be used to set an explicit path, but in
372
    // normal mode, the project variable `CMAKE_EXPORT_SARIF` can also enable
373
    // SARIF logging.
374
1
    return cm.GetSarifFilePath().has_value() ||
375
1
      (cm.GetState()->GetRole() == cmState::Role::Project &&
376
0
       cm.GetCacheDefinition(cmSarif::PROJECT_SARIF_FILE_VARIABLE).IsOn());
377
1
  });
378
379
1
  return true;
380
1
}