/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 | } |