Coverage Report

Created: 2026-07-14 07:09

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
cm::optional<cmSarif::Location> GetLocationFromBacktrace(
35
  cmListFileBacktrace const& backtrace, cmake const& cm)
36
0
{
37
0
  if (backtrace.Empty()) {
38
0
    return {};
39
0
  }
40
0
  cmListFileContext const& lfc = backtrace.Top();
41
  // Exclude frames with no real location: negative lines are deferred-call
42
  // placeholders, and LONG_MAX is the synthetic line used by variable_watch
43
  // callback dispatch. Neither is a meaningful source location.
44
0
  if (lfc.Line < 0 || lfc.Line == std::numeric_limits<long>::max()) {
45
0
    return {};
46
0
  }
47
48
0
  cmSarif::PhysicalLocation location;
49
0
  location.Artifact.Uri = lfc.FilePath;
50
51
  // SARIF requests that paths are given relative to a logical base. Report
52
  // paths relative to the source dir / script working directory if possible.
53
0
  location.Artifact.UriBaseId = cm.GetHomeDirectory();
54
0
  std::string relative = cmSystemTools::RelativePath(
55
0
    location.Artifact.UriBaseId, location.Artifact.Uri);
56
0
  if (!relative.empty()) {
57
0
    location.Artifact.Uri = relative;
58
0
  }
59
60
0
  if (lfc.Line != 0) {
61
0
    cmSarif::Region region;
62
0
    region.StartLine = lfc.Line;
63
0
    location.ArtifactRegion = region;
64
0
  }
65
0
  return cmSarif::Location{ location };
66
0
}
67
68
cmSarif::Tool CreateCMakeTool()
69
0
{
70
0
  cmSarif::ToolComponent cmDriver;
71
0
  cmDriver.Name = "CMake";
72
0
  cmDriver.Version = CMake_VERSION;
73
74
0
  return cmSarif::Tool{ cmDriver };
75
0
}
76
77
std::string RuleIdForMessageType(MessageType type,
78
                                 cmDiagnosticCategory category)
79
0
{
80
0
  cm::string_view name = cmDiagnostics::GetCategoryString(category);
81
0
  if (!name.empty()) {
82
    // Strip the "CMD_" prefix from the category name and convert to PascalCase
83
0
    std::string sarifIdName;
84
0
    bool nextWord = true;
85
0
    for (char c : name.substr(4)) {
86
0
      if (c == '_') {
87
0
        nextWord = true;
88
0
        continue;
89
0
      }
90
0
      if (nextWord) {
91
0
        sarifIdName += c;
92
0
        nextWord = false;
93
0
      } else {
94
0
        sarifIdName += cmsysString_tolower(c);
95
0
      }
96
0
    }
97
0
    return cmStrCat("CMake.", sarifIdName);
98
0
  }
99
100
  // Fall back to message type if not a diagnostic
101
0
  switch (type) {
102
0
    case MessageType::FATAL_ERROR:
103
0
      return "CMake.FatalError";
104
0
    case MessageType::INTERNAL_ERROR:
105
0
      return "CMake.InternalError";
106
0
    case MessageType::WARNING:
107
0
      return "CMake.Warning";
108
0
    default:
109
0
      return "";
110
0
  }
111
0
}
112
113
cm::string_view NameForMessageType(MessageType type,
114
                                   cmDiagnosticCategory category)
115
0
{
116
0
  if (category != cmDiagnostics::CMD_NONE) {
117
0
    return cmDiagnostics::GetCategoryString(category);
118
0
  }
119
0
  switch (type) {
120
0
    case MessageType::FATAL_ERROR:
121
0
      return "CMake Error";
122
0
    case MessageType::INTERNAL_ERROR:
123
0
      return "CMake Internal Error";
124
0
    case MessageType::WARNING:
125
0
      return "CMake Warning";
126
0
    default:
127
0
      return "";
128
0
  }
129
0
}
130
131
cmSarif::ReportingDescriptor ReportingDescriptorForMessageType(
132
  MessageType type, cmDiagnosticCategory category)
133
0
{
134
0
  cmSarif::ReportingDescriptor rd;
135
0
  rd.Id = RuleIdForMessageType(type, category);
136
0
  rd.Name = NameForMessageType(type, category);
137
0
  return rd;
138
0
};
139
140
cmSarif::ResultSeverityLevel SarifLevelFromMessageType(MessageType type)
141
0
{
142
0
  switch (type) {
143
0
    case MessageType::FATAL_ERROR:
144
0
    case MessageType::INTERNAL_ERROR:
145
0
      return cmSarif::ResultSeverityLevel::Error;
146
0
    case MessageType::WARNING:
147
0
      return cmSarif::ResultSeverityLevel::Warning;
148
0
    default:
149
0
      return cmSarif::ResultSeverityLevel::Note;
150
0
  }
151
0
}
152
153
} // namespace
154
155
cmCMakeSarifLogger::cmCMakeSarifLogger(cmake& cm)
156
1
  : CM(cm)
157
1
{
158
1
  if (this->CM.GetState()->GetRole() == cmState::Role::Project) {
159
0
    cm.MarkCliAsUsed(CMakeSarifOutputFlag);
160
0
  }
161
1
}
162
163
cmCMakeSarifLogger::~cmCMakeSarifLogger()
164
1
{
165
1
  this->GenerateForRun();
166
1
}
167
168
cm::optional<std::string> cmCMakeSarifLogger::FileOutputPath() const
169
1
{
170
  // If a SARIF path was specified via CLI, use it. Otherwise, check whether
171
  // logging is enabled via the project cache variable and use the default
172
  // path if so.
173
1
  if (cm::optional<std::string> specifiedPath = this->CM.GetSarifFilePath()) {
174
0
    return specifiedPath;
175
0
  }
176
1
  if (this->CM.GetState()->GetRole() == cmState::Role::Project &&
177
0
      this->CM.GetCacheDefinition(CMakeSarifOutputFlag).IsOn()) {
178
0
    return cmStrCat(this->CM.GetHomeOutputDirectory(), '/', DefaultSarifFile);
179
0
  }
180
1
  return cm::nullopt;
181
1
}
182
183
bool cmCMakeSarifLogger::WriteFile(std::string const& path,
184
                                   bool createParentDirectories) const
185
0
{
186
0
  if (createParentDirectories) {
187
0
    if (!cmSystemTools::MakeDirectory(cmSystemTools::GetFilenamePath(path))
188
0
           .IsSuccess()) {
189
0
      return false;
190
0
    }
191
0
  }
192
193
0
  cmsys::ofstream outputFile(path);
194
0
  if (!outputFile.good()) {
195
0
    return false;
196
0
  }
197
198
  // Run object to build
199
0
  cmSarif::Run run;
200
0
  run.Tool = CreateCMakeTool();
201
202
  // Helper to add rules to the run as encountered in results and get their
203
  // index for reporting
204
0
  std::unordered_map<std::string, std::size_t> ruleIndices;
205
0
  auto use_rule = [&](MessageType type, cmDiagnosticCategory category) {
206
0
    std::string category_name = RuleIdForMessageType(type, category);
207
0
    auto result = ruleIndices.emplace(category_name, 0);
208
0
    if (result.second) {
209
0
      result.first->second = run.Tool.Driver.Rules.size();
210
0
      run.Tool.Driver.Rules.emplace_back(
211
0
        ReportingDescriptorForMessageType(type, category));
212
0
    }
213
0
    return *result.first;
214
0
  };
215
216
0
  cmMessenger const& messenger = *this->CM.GetMessenger();
217
0
  for (auto const& message : messenger.GetDisplayedMessages()) {
218
    // SARIF should only emit diagnostic messages, not general messages/logs
219
0
    switch (message.Type) {
220
0
      case MessageType::MESSAGE:
221
0
      case MessageType::LOG:
222
0
      case MessageType::UNDEFINED:
223
0
        continue;
224
0
      default:
225
0
        break;
226
0
    }
227
228
0
    std::pair<std::string, std::size_t> ruleInfo =
229
0
      use_rule(message.Type, message.Category);
230
231
0
    cmSarif::Result result;
232
0
    result.RuleId = ruleInfo.first;
233
0
    result.RuleIndex = ruleInfo.second;
234
0
    result.Message = message.Text;
235
0
    result.Location = GetLocationFromBacktrace(message.Backtrace, this->CM);
236
0
    result.Level = SarifLevelFromMessageType(message.Type);
237
238
0
    run.Results.emplace_back(std::move(result));
239
0
  }
240
241
0
  return cmSarif::WriteLog(path, run);
242
0
}
243
244
void cmCMakeSarifLogger::GenerateForRun() const
245
1
{
246
1
  cm::optional<std::string> path = this->FileOutputPath();
247
1
  if (!path) {
248
1
    return;
249
1
  }
250
251
  // If using the default path within the build dir, ensure parents are created
252
0
  bool const createParents = !this->CM.GetSarifFilePath().has_value();
253
0
  if (!this->WriteFile(*path, createParents)) {
254
0
    cmSystemTools::Error(cmStrCat("Failed to write SARIF log to ", *path));
255
0
  }
256
0
}