Coverage Report

Created: 2026-04-29 07:01

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::WARNING:
128
0
      return ResultSeverityLevel::SARIF_WARNING;
129
1
    case MessageType::FATAL_ERROR:
130
1
    case MessageType::INTERNAL_ERROR:
131
1
      return ResultSeverityLevel::SARIF_ERROR;
132
0
    case MessageType::MESSAGE:
133
0
    case MessageType::LOG:
134
0
      return ResultSeverityLevel::SARIF_NOTE;
135
0
    default:
136
0
      return ResultSeverityLevel::SARIF_NONE;
137
1
  }
138
1
}
139
140
cm::optional<std::string> cmSarif::MessageRuleId(MessageType t)
141
1
{
142
1
  switch (t) {
143
0
    case MessageType::WARNING:
144
0
      return "CMake.Warning";
145
1
    case MessageType::FATAL_ERROR:
146
1
      return "CMake.FatalError";
147
0
    case MessageType::INTERNAL_ERROR:
148
0
      return "CMake.InternalError";
149
0
    case MessageType::MESSAGE:
150
0
      return "CMake.Message";
151
0
    case MessageType::LOG:
152
0
      return "CMake.Log";
153
0
    default:
154
0
      return cm::nullopt;
155
1
  }
156
1
}
157
158
Json::Value cmSarif::Rule::GetJson() const
159
0
{
160
0
  Json::Value rule(Json::objectValue);
161
0
  rule["id"] = this->Id;
162
163
0
  if (this->Name) {
164
0
    rule["name"] = *this->Name;
165
0
  }
166
0
  if (this->FullDescription) {
167
0
    rule["fullDescription"]["text"] = *this->FullDescription;
168
0
  }
169
0
  if (this->DefaultMessage) {
170
0
    rule["messageStrings"]["default"]["text"] = *this->DefaultMessage;
171
0
  }
172
173
0
  return rule;
174
0
}
175
176
cmSarif::SourceFileLocation::SourceFileLocation(
177
  cmListFileBacktrace const& backtrace)
178
1
{
179
1
  if (backtrace.Empty()) {
180
0
    throw std::runtime_error("Empty source file location");
181
0
  }
182
183
1
  cmListFileContext const& lfc = backtrace.Top();
184
1
  this->Uri = lfc.FilePath;
185
1
  this->Line = lfc.Line;
186
1
}
187
188
cm::optional<cmSarif::SourceFileLocation>
189
cmSarif::SourceFileLocation::FromBacktrace(
190
  cmListFileBacktrace const& backtrace)
191
1
{
192
1
  if (backtrace.Empty()) {
193
0
    return cm::nullopt;
194
0
  }
195
1
  cmListFileContext const& lfc = backtrace.Top();
196
1
  if (lfc.Line <= 0 || lfc.FilePath.empty()) {
197
0
    return cm::nullopt;
198
0
  }
199
200
1
  return cm::make_optional<cmSarif::SourceFileLocation>(backtrace);
201
1
}
202
203
void cmSarif::ResultsLog::WriteJson(Json::Value& root) const
204
0
{
205
  // Add SARIF metadata
206
0
  root["version"] = "2.1.0";
207
0
  root["$schema"] = "https://schemastore.azurewebsites.net/schemas/json/"
208
0
                    "sarif-2.1.0-rtm.4.json";
209
210
  // JSON object for the SARIF runs array
211
0
  Json::Value runs(Json::arrayValue);
212
213
  // JSON object for the current (only) run
214
0
  Json::Value currentRun(Json::objectValue);
215
216
  // Accumulate info about the reported rules
217
0
  Json::Value jsonRules(Json::arrayValue);
218
0
  for (auto const& ruleId : this->EnabledRules) {
219
0
    jsonRules.append(KnownRules.at(ruleId).GetJson());
220
0
  }
221
222
  // Add info the driver for the current run (CMake)
223
0
  Json::Value driverTool(Json::objectValue);
224
0
  driverTool["name"] = "CMake";
225
0
  driverTool["version"] = CMake_VERSION;
226
0
  driverTool["rules"] = jsonRules;
227
0
  currentRun["tool"]["driver"] = driverTool;
228
229
0
  runs.append(currentRun);
230
231
  // Add all results
232
0
  Json::Value jsonResults(Json::arrayValue);
233
0
  for (auto const& res : this->Results) {
234
0
    Json::Value jsonResult(Json::objectValue);
235
236
0
    if (res.Message) {
237
0
      jsonResult["message"]["text"] = *(res.Message);
238
0
    }
239
240
    // If the result has a level, add it to the result
241
0
    if (res.Level) {
242
0
      switch (*res.Level) {
243
0
        case ResultSeverityLevel::SARIF_WARNING:
244
0
          jsonResult["level"] = "warning";
245
0
          break;
246
0
        case ResultSeverityLevel::SARIF_ERROR:
247
0
          jsonResult["level"] = "error";
248
0
          break;
249
0
        case ResultSeverityLevel::SARIF_NOTE:
250
0
          jsonResult["level"] = "note";
251
0
          break;
252
0
        case ResultSeverityLevel::SARIF_NONE:
253
0
          jsonResult["level"] = "none";
254
0
          break;
255
0
      }
256
0
    }
257
258
    // If the result has a rule ID or index, add it to the result
259
0
    if (res.RuleId) {
260
0
      jsonResult["ruleId"] = *res.RuleId;
261
0
    }
262
0
    if (res.RuleIndex) {
263
0
      jsonResult["ruleIndex"] = Json::UInt64(*res.RuleIndex);
264
0
    }
265
266
0
    if (res.Location) {
267
0
      jsonResult["locations"][0]["physicalLocation"]["artifactLocation"]
268
0
                ["uri"] = (res.Location)->Uri;
269
0
      jsonResult["locations"][0]["physicalLocation"]["region"]["startLine"] =
270
0
        Json::Int64((res.Location)->Line);
271
0
    }
272
273
0
    jsonResults.append(jsonResult);
274
0
  }
275
276
0
  currentRun["results"] = jsonResults;
277
0
  runs[0] = currentRun;
278
0
  root["runs"] = runs;
279
0
}
280
281
cmSarif::LogFileWriter::~LogFileWriter()
282
1
{
283
  // If the file has not been written yet, try to finalize it
284
1
  if (!this->FileWritten) {
285
    // Try to write and check the result
286
1
    if (this->TryWrite() == WriteResult::FAILURE) {
287
      // If the result is `FAILURE`, it means the write condition is true but
288
      // the file still wasn't written. This is an error.
289
0
      cmSystemTools::Error("Failed to write SARIF log to " + this->FilePath);
290
0
    }
291
1
  }
292
1
}
293
294
bool cmSarif::LogFileWriter::EnsureFileValid()
295
0
{
296
  // First, ensure directory exists
297
0
  std::string const dir = cmSystemTools::GetFilenamePath(this->FilePath);
298
0
  if (!cmSystemTools::FileIsDirectory(dir)) {
299
0
    if (!this->CreateDirectories ||
300
0
        !cmSystemTools::MakeDirectory(dir).IsSuccess()) {
301
0
      return false;
302
0
    }
303
0
  }
304
305
  // Open the file for writing
306
0
  cmsys::ofstream outputFile(this->FilePath.c_str());
307
0
  if (!outputFile.good()) {
308
0
    return false;
309
0
  }
310
0
  return true;
311
0
}
312
313
cmSarif::LogFileWriter::WriteResult cmSarif::LogFileWriter::TryWrite()
314
1
{
315
  // Check that SARIF logging is enabled
316
1
  if (!this->WriteCondition || !this->WriteCondition()) {
317
1
    return WriteResult::SKIPPED;
318
1
  }
319
320
  // Open the file
321
0
  if (!this->EnsureFileValid()) {
322
0
    return WriteResult::FAILURE;
323
0
  }
324
0
  cmsys::ofstream outputFile(this->FilePath.c_str());
325
326
  // The file is available, so proceed to write the log
327
328
  // Assemble the SARIF JSON from the results in the log
329
0
  Json::Value root(Json::objectValue);
330
0
  this->Log.WriteJson(root);
331
332
  // Serialize the JSON to the file
333
0
  Json::StreamWriterBuilder builder;
334
0
  std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
335
336
0
  writer->write(root, &outputFile);
337
0
  outputFile.close();
338
339
0
  this->FileWritten = true;
340
0
  return WriteResult::SUCCESS;
341
0
}
342
343
bool cmSarif::LogFileWriter::ConfigureForCMakeRun(cmake& cm)
344
1
{
345
  // If an explicit SARIF output path has been provided, set and check it
346
1
  if (cm::optional<std::string> sarifFilePath = cm.GetSarifFilePath()) {
347
0
    this->SetPath(*sarifFilePath);
348
0
    if (!this->EnsureFileValid()) {
349
0
      cmSystemTools::Error(
350
0
        cmStrCat("Invalid SARIF output file path: ", *sarifFilePath));
351
0
      return false;
352
0
    }
353
0
  }
354
355
  // The write condition is checked immediately before writing the file, which
356
  // allows projects to enable SARIF diagnostics by setting a cache variable
357
  // and have it take effect for the current run.
358
1
  this->SetWriteCondition([&cm]() {
359
    // The command-line option can be used to set an explicit path, but in
360
    // normal mode, the project variable `CMAKE_EXPORT_SARIF` can also enable
361
    // SARIF logging.
362
1
    return cm.GetSarifFilePath().has_value() ||
363
1
      (cm.GetState()->GetRole() == cmState::Role::Project &&
364
0
       cm.GetCacheDefinition(cmSarif::PROJECT_SARIF_FILE_VARIABLE).IsOn());
365
1
  });
366
367
1
  return true;
368
1
}