CMake/Source/cmSarifLog.cxx
Kitware Robot 1772622772 LICENSE: Replace references to Copyright.txt with LICENSE.rst
```
git grep -lz 'Copyright.txt or https://cmake.org/licensing ' |
  while IFS= read -r -d $'\0' f ; do
    sed -i '/Copyright.txt or https:\/\/cmake.org\/licensing / {
              s/Copyright.txt/LICENSE.rst/
            }' "$f" ; done
```
2025-03-03 10:43:35 -05:00

384 lines
12 KiB
C++

/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#include "cmSarifLog.h"
#include <memory>
#include <stdexcept>
#include <cm/filesystem>
#include <cm3p/json/value.h>
#include <cm3p/json/writer.h>
#include "cmsys/FStream.hxx"
#include "cmListFileCache.h"
#include "cmMessageType.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmValue.h"
#include "cmVersionConfig.h"
#include "cmake.h"
cmSarif::ResultsLog::ResultsLog()
{
// Add the known CMake rules
this->KnownRules.emplace(RuleBuilder("CMake.AuthorWarning")
.Name("CMake Warning (dev)")
.DefaultMessage("CMake Warning (dev): {0}")
.Build());
this->KnownRules.emplace(RuleBuilder("CMake.Warning")
.Name("CMake Warning")
.DefaultMessage("CMake Warning: {0}")
.Build());
this->KnownRules.emplace(RuleBuilder("CMake.DeprecationWarning")
.Name("CMake Deprecation Warning")
.DefaultMessage("CMake Deprecation Warning: {0}")
.Build());
this->KnownRules.emplace(RuleBuilder("CMake.AuthorError")
.Name("CMake Error (dev)")
.DefaultMessage("CMake Error (dev): {0}")
.Build());
this->KnownRules.emplace(RuleBuilder("CMake.FatalError")
.Name("CMake Error")
.DefaultMessage("CMake Error: {0}")
.Build());
this->KnownRules.emplace(
RuleBuilder("CMake.InternalError")
.Name("CMake Internal Error")
.DefaultMessage("CMake Internal Error (please report a bug): {0}")
.Build());
this->KnownRules.emplace(RuleBuilder("CMake.DeprecationError")
.Name("CMake Deprecation Error")
.DefaultMessage("CMake Deprecation Error: {0}")
.Build());
this->KnownRules.emplace(RuleBuilder("CMake.Message")
.Name("CMake Message")
.DefaultMessage("CMake Message: {0}")
.Build());
this->KnownRules.emplace(RuleBuilder("CMake.Log")
.Name("CMake Log")
.DefaultMessage("CMake Log: {0}")
.Build());
}
void cmSarif::ResultsLog::Log(cmSarif::Result&& result) const
{
// The rule ID is optional, but if it is present, enable metadata output for
// the rule by marking it as used
if (result.RuleId) {
std::size_t index = this->UseRule(*result.RuleId);
result.RuleIndex = index;
}
// Add the result to the log
this->Results.emplace_back(result);
}
void cmSarif::ResultsLog::LogMessage(
MessageType t, std::string const& text,
cmListFileBacktrace const& backtrace) const
{
// Add metadata to the result object
// The CMake SARIF rules for messages all expect 1 string argument with the
// message text
Json::Value additionalProperties(Json::objectValue);
Json::Value args(Json::arrayValue);
args.append(text);
additionalProperties["message"]["id"] = "default";
additionalProperties["message"]["arguments"] = args;
// Create and log a result object
// Rule indices are assigned when writing the final JSON output. Right now,
// leave it as nullopt. The other optional fields are filled if available
this->Log(cmSarif::Result{
text, cmSarif::SourceFileLocation::FromBacktrace(backtrace),
cmSarif::MessageSeverityLevel(t), cmSarif::MessageRuleId(t), cm::nullopt,
additionalProperties });
}
std::size_t cmSarif::ResultsLog::UseRule(std::string const& id) const
{
// Check if the rule is already in the index
auto it = this->RuleToIndex.find(id);
if (it != this->RuleToIndex.end()) {
// The rule is already in use. Return the known index
return it->second;
}
// This rule is not yet in the index, so check if it is recognized
auto itKnown = this->KnownRules.find(id);
if (itKnown == this->KnownRules.end()) {
// The rule is not known. Add an empty rule to the known rules so that it
// is included in the output
this->KnownRules.emplace(RuleBuilder(id.c_str()).Build());
}
// Since this is the first time the rule is used, enable it and add it to the
// index
std::size_t idx = this->EnabledRules.size();
this->RuleToIndex[id] = idx;
this->EnabledRules.emplace_back(id);
return idx;
}
cmSarif::ResultSeverityLevel cmSarif::MessageSeverityLevel(MessageType t)
{
switch (t) {
case MessageType::AUTHOR_WARNING:
case MessageType::WARNING:
case MessageType::DEPRECATION_WARNING:
return ResultSeverityLevel::SARIF_WARNING;
case MessageType::AUTHOR_ERROR:
case MessageType::FATAL_ERROR:
case MessageType::INTERNAL_ERROR:
case MessageType::DEPRECATION_ERROR:
return ResultSeverityLevel::SARIF_ERROR;
case MessageType::MESSAGE:
case MessageType::LOG:
return ResultSeverityLevel::SARIF_NOTE;
default:
return ResultSeverityLevel::SARIF_NONE;
}
}
cm::optional<std::string> cmSarif::MessageRuleId(MessageType t)
{
switch (t) {
case MessageType::AUTHOR_WARNING:
return "CMake.AuthorWarning";
case MessageType::WARNING:
return "CMake.Warning";
case MessageType::DEPRECATION_WARNING:
return "CMake.DeprecationWarning";
case MessageType::AUTHOR_ERROR:
return "CMake.AuthorError";
case MessageType::FATAL_ERROR:
return "CMake.FatalError";
case MessageType::INTERNAL_ERROR:
return "CMake.InternalError";
case MessageType::DEPRECATION_ERROR:
return "CMake.DeprecationError";
case MessageType::MESSAGE:
return "CMake.Message";
case MessageType::LOG:
return "CMake.Log";
default:
return cm::nullopt;
}
}
Json::Value cmSarif::Rule::GetJson() const
{
Json::Value rule(Json::objectValue);
rule["id"] = this->Id;
if (this->Name) {
rule["name"] = *this->Name;
}
if (this->FullDescription) {
rule["fullDescription"]["text"] = *this->FullDescription;
}
if (this->DefaultMessage) {
rule["messageStrings"]["default"]["text"] = *this->DefaultMessage;
}
return rule;
}
cmSarif::SourceFileLocation::SourceFileLocation(
cmListFileBacktrace const& backtrace)
{
if (backtrace.Empty()) {
throw std::runtime_error("Empty source file location");
}
cmListFileContext const& lfc = backtrace.Top();
this->Uri = lfc.FilePath;
this->Line = lfc.Line;
}
cm::optional<cmSarif::SourceFileLocation>
cmSarif::SourceFileLocation::FromBacktrace(
cmListFileBacktrace const& backtrace)
{
if (backtrace.Empty()) {
return cm::nullopt;
}
cmListFileContext const& lfc = backtrace.Top();
if (lfc.Line <= 0 || lfc.FilePath.empty()) {
return cm::nullopt;
}
return cm::make_optional<cmSarif::SourceFileLocation>(backtrace);
}
void cmSarif::ResultsLog::WriteJson(Json::Value& root) const
{
// Add SARIF metadata
root["version"] = "2.1.0";
root["$schema"] = "https://schemastore.azurewebsites.net/schemas/json/"
"sarif-2.1.0-rtm.4.json";
// JSON object for the SARIF runs array
Json::Value runs(Json::arrayValue);
// JSON object for the current (only) run
Json::Value currentRun(Json::objectValue);
// Accumulate info about the reported rules
Json::Value jsonRules(Json::arrayValue);
for (auto const& ruleId : this->EnabledRules) {
jsonRules.append(KnownRules.at(ruleId).GetJson());
}
// Add info the driver for the current run (CMake)
Json::Value driverTool(Json::objectValue);
driverTool["name"] = "CMake";
driverTool["version"] = CMake_VERSION;
driverTool["rules"] = jsonRules;
currentRun["tool"]["driver"] = driverTool;
runs.append(currentRun);
// Add all results
Json::Value jsonResults(Json::arrayValue);
for (auto const& res : this->Results) {
Json::Value jsonResult(Json::objectValue);
if (res.Message) {
jsonResult["message"]["text"] = *(res.Message);
}
// If the result has a level, add it to the result
if (res.Level) {
switch (*res.Level) {
case ResultSeverityLevel::SARIF_WARNING:
jsonResult["level"] = "warning";
break;
case ResultSeverityLevel::SARIF_ERROR:
jsonResult["level"] = "error";
break;
case ResultSeverityLevel::SARIF_NOTE:
jsonResult["level"] = "note";
break;
case ResultSeverityLevel::SARIF_NONE:
jsonResult["level"] = "none";
break;
}
}
// If the result has a rule ID or index, add it to the result
if (res.RuleId) {
jsonResult["ruleId"] = *res.RuleId;
}
if (res.RuleIndex) {
jsonResult["ruleIndex"] = Json::UInt64(*res.RuleIndex);
}
if (res.Location) {
jsonResult["locations"][0]["physicalLocation"]["artifactLocation"]
["uri"] = (res.Location)->Uri;
jsonResult["locations"][0]["physicalLocation"]["region"]["startLine"] =
Json::Int64((res.Location)->Line);
}
jsonResults.append(jsonResult);
}
currentRun["results"] = jsonResults;
runs[0] = currentRun;
root["runs"] = runs;
}
cmSarif::LogFileWriter::~LogFileWriter()
{
// If the file has not been written yet, try to finalize it
if (!this->FileWritten) {
// Try to write and check the result
if (this->TryWrite() == WriteResult::FAILURE) {
// If the result is `FAILURE`, it means the write condition is true but
// the file still wasn't written. This is an error.
cmSystemTools::Error("Failed to write SARIF log to " +
this->FilePath.generic_string());
}
}
}
bool cmSarif::LogFileWriter::EnsureFileValid()
{
// First, ensure directory exists
cm::filesystem::path dir = this->FilePath.parent_path();
if (!cmSystemTools::FileIsDirectory(dir.generic_string())) {
if (!this->CreateDirectories ||
!cmSystemTools::MakeDirectory(dir.generic_string()).IsSuccess()) {
return false;
}
}
// Open the file for writing
cmsys::ofstream outputFile(this->FilePath.generic_string().c_str());
if (!outputFile.good()) {
return false;
}
return true;
}
cmSarif::LogFileWriter::WriteResult cmSarif::LogFileWriter::TryWrite()
{
// Check that SARIF logging is enabled
if (!this->WriteCondition || !this->WriteCondition()) {
return WriteResult::SKIPPED;
}
// Open the file
if (!this->EnsureFileValid()) {
return WriteResult::FAILURE;
}
cmsys::ofstream outputFile(this->FilePath.generic_string().c_str());
// The file is available, so proceed to write the log
// Assemble the SARIF JSON from the results in the log
Json::Value root(Json::objectValue);
this->Log.WriteJson(root);
// Serialize the JSON to the file
Json::StreamWriterBuilder builder;
std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
writer->write(root, &outputFile);
outputFile.close();
this->FileWritten = true;
return WriteResult::SUCCESS;
}
bool cmSarif::LogFileWriter::ConfigureForCMakeRun(cmake& cm)
{
// If an explicit SARIF output path has been provided, set and check it
cm::optional<std::string> sarifFilePath = cm.GetSarifFilePath();
if (sarifFilePath) {
this->SetPath(cm::filesystem::path(*sarifFilePath));
if (!this->EnsureFileValid()) {
cmSystemTools::Error(
cmStrCat("Invalid SARIF output file path: ", *sarifFilePath));
return false;
}
}
// The write condition is checked immediately before writing the file, which
// allows projects to enable SARIF diagnostics by setting a cache variable
// and have it take effect for the current run.
this->SetWriteCondition([&cm]() {
// The command-line option can be used to set an explicit path, but in
// normal mode, the project variable `CMAKE_EXPORT_SARIF` can also enable
// SARIF logging.
return cm.GetSarifFilePath().has_value() ||
(cm.GetWorkingMode() == cmake::NORMAL_MODE &&
cm.GetCacheDefinition(cmSarif::PROJECT_SARIF_FILE_VARIABLE).IsOn());
});
return true;
}