/* 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 #include #include #include #include #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 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::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(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 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 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; }