|
@@ -0,0 +1,383 @@
|
|
|
|
+/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
|
|
|
|
+ file Copyright.txt 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;
|
|
|
|
+}
|