Browse Source

Merge topic 'cmake_sarif_output'

abbe41578d cmake: Add SARIF diagnostics output support

Acked-by: Kitware Robot <[email protected]>
Acked-by: buildbot <[email protected]>
Acked-by: scivision <[email protected]>
Merge-request: !10139
Brad King 8 months ago
parent
commit
a832d31474
37 changed files with 1085 additions and 0 deletions
  1. 1 0
      Auxiliary/vim/syntax/cmake.vim
  2. 1 0
      Help/manual/cmake-variables.7.rst
  3. 10 0
      Help/manual/cmake.1.rst
  4. 81 0
      Help/variable/CMAKE_EXPORT_SARIF.rst
  5. 2 0
      Source/CMakeLists.txt
  6. 7 0
      Source/cmMessenger.cxx
  7. 12 0
      Source/cmMessenger.h
  8. 383 0
      Source/cmSarifLog.cxx
  9. 287 0
      Source/cmSarifLog.h
  10. 39 0
      Source/cmake.cxx
  11. 16 0
      Source/cmake.h
  12. 1 0
      Tests/RunCMake/CMakeLists.txt
  13. 3 0
      Tests/RunCMake/SarifOutput/CMakeLists.txt
  14. 4 0
      Tests/RunCMake/SarifOutput/DefaultSarifOutput-check.cmake
  15. 4 0
      Tests/RunCMake/SarifOutput/DefaultSarifOutput-stderr.txt
  16. 1 0
      Tests/RunCMake/SarifOutput/DefaultSarifOutput.cmake
  17. 4 0
      Tests/RunCMake/SarifOutput/GenerateSarifResults-check.cmake
  18. 67 0
      Tests/RunCMake/SarifOutput/GenerateSarifResults-expected.sarif
  19. 9 0
      Tests/RunCMake/SarifOutput/GenerateSarifResults-stderr.txt
  20. 8 0
      Tests/RunCMake/SarifOutput/GenerateSarifResults.cmake
  21. 4 0
      Tests/RunCMake/SarifOutput/ProjectFatalError-check.cmake
  22. 47 0
      Tests/RunCMake/SarifOutput/ProjectFatalError-expected.sarif
  23. 1 0
      Tests/RunCMake/SarifOutput/ProjectFatalError-result.txt
  24. 4 0
      Tests/RunCMake/SarifOutput/ProjectFatalError-stderr.txt
  25. 1 0
      Tests/RunCMake/SarifOutput/ProjectFatalError.cmake
  26. 25 0
      Tests/RunCMake/SarifOutput/RunCMakeTest.cmake
  27. 4 0
      Tests/RunCMake/SarifOutput/SarifFileArgument-check.cmake
  28. 4 0
      Tests/RunCMake/SarifOutput/SarifFileArgument-stderr.txt
  29. 1 0
      Tests/RunCMake/SarifOutput/SarifFileArgument.cmake
  30. 4 0
      Tests/RunCMake/SarifOutput/SarifFileArgumentScript-check.cmake
  31. 2 0
      Tests/RunCMake/SarifOutput/SarifFileArgumentScript.cmake
  32. 4 0
      Tests/RunCMake/SarifOutput/ScriptModeSarifVariable-check.cmake
  33. 3 0
      Tests/RunCMake/SarifOutput/ScriptModeSarifVariable.cmake
  34. 5 0
      Tests/RunCMake/SarifOutput/ToggleExportSarifVariable-check.cmake
  35. 11 0
      Tests/RunCMake/SarifOutput/ToggleExportSarifVariable-stderr.txt
  36. 6 0
      Tests/RunCMake/SarifOutput/ToggleExportSarifVariable.cmake
  37. 19 0
      Tests/RunCMake/SarifOutput/check-sarif.cmake

+ 1 - 0
Auxiliary/vim/syntax/cmake.vim

@@ -1180,6 +1180,7 @@ syn keyword cmakeVariable contained
             \ CMAKE_EXE_LINKER_FLAGS
             \ CMAKE_EXE_LINKER_FLAGS_INIT
             \ CMAKE_EXPORT_COMPILE_COMMANDS
+            \ CMAKE_EXPORT_SARIF
             \ CMAKE_EXPORT_NO_PACKAGE_REGISTRY
             \ CMAKE_EXPORT_PACKAGE_REGISTRY
             \ CMAKE_EXTRA_GENERATOR

+ 1 - 0
Help/manual/cmake-variables.7.rst

@@ -204,6 +204,7 @@ Variables that Change Behavior
    /variable/CMAKE_EXECUTE_PROCESS_COMMAND_ERROR_IS_FATAL
    /variable/CMAKE_EXPORT_BUILD_DATABASE
    /variable/CMAKE_EXPORT_COMPILE_COMMANDS
+   /variable/CMAKE_EXPORT_SARIF
    /variable/CMAKE_EXPORT_PACKAGE_REGISTRY
    /variable/CMAKE_EXPORT_NO_PACKAGE_REGISTRY
    /variable/CMAKE_FIND_APPBUNDLE

+ 10 - 0
Help/manual/cmake.1.rst

@@ -307,6 +307,16 @@ Options
  When this command line option is given, :variable:`CMAKE_MESSAGE_CONTEXT_SHOW`
  is ignored.
 
+.. option:: --sarif-output=<path>
+
+ .. versionadded:: 4.0
+
+ Enable logging of diagnostic messages produced by CMake in the SARIF format.
+
+ Write diagnostic messages to a SARIF file at the path specified. Projects can
+ also set :variable:`CMAKE_EXPORT_SARIF` to ``ON`` to enable this feature for a
+ build tree.
+
 .. option:: --debug-trycompile
 
  Do not delete the files and directories created for

+ 81 - 0
Help/variable/CMAKE_EXPORT_SARIF.rst

@@ -0,0 +1,81 @@
+CMAKE_EXPORT_SARIF
+------------------
+
+.. versionadded:: 4.0
+
+Enable or disable CMake diagnostics output in SARIF format for a project.
+
+If enabled, CMake will generate a SARIF log file containing diagnostic messages
+output by CMake when running in a project. By default, the log file is written
+to `.cmake/sarif/cmake.sarif`, but the location can be changed by setting the
+command-line option :option:`cmake --sarif-output` to the desired path.
+
+The Static Analysis Results Interchange Format (SARIF) is a JSON-based standard
+format for static analysis tools (including build tools like CMake) to record
+and communicate diagnostic messages. CMake generates a SARIF log entry for
+warnings and errors produced while running CMake on a project (e.g.
+:command:`message` calls). Each log entry includes the message, severity, and
+location information if available.
+
+An example of CMake's SARIF output is:
+
+.. code-block:: json
+
+  {
+    "version" : "2.1.0",
+    "$schema" : "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.4.json",
+    "runs" :
+    [
+      {
+        "tool" :
+        {
+          "driver" :
+          {
+            "name" : "CMake",
+            "rules" :
+            [
+              {
+                "id" : "CMake.Warning",
+                "messageStrings" :
+                {
+                  "default" :
+                  {
+                    "text" : "CMake Warning: {0}"
+                  }
+                },
+                "name" : "CMake Warning"
+              }
+            ]
+          }
+        },
+        "results" :
+        [
+          {
+            "level" : "warning",
+            "locations" :
+            [
+              {
+                "physicalLocation" :
+                {
+                  "artifactLocation" :
+                  {
+                    "uri" : "/home/user/development/project/CMakeLists.txt"
+                  },
+                  "region" :
+                  {
+                    "startLine" : 5
+                  }
+                }
+              }
+            ],
+            "message" :
+            {
+              "text" : "An example warning"
+            },
+            "ruleId" : "CMake.Warning",
+            "ruleIndex" : 0
+          }
+        ]
+      }
+    ]
+  }

+ 2 - 0
Source/CMakeLists.txt

@@ -446,6 +446,8 @@ add_library(
   cmRST.h
   cmRuntimeDependencyArchive.cxx
   cmRuntimeDependencyArchive.h
+  cmSarifLog.cxx
+  cmSarifLog.h
   cmScriptGenerator.h
   cmScriptGenerator.cxx
   cmSourceFile.cxx

+ 7 - 0
Source/cmMessenger.cxx

@@ -10,6 +10,8 @@
 
 #if !defined(CMAKE_BOOTSTRAP)
 #  include "cmsys/SystemInformation.hxx"
+
+#  include "cmSarifLog.h"
 #endif
 
 #include <sstream>
@@ -218,6 +220,11 @@ void cmMessenger::DisplayMessage(MessageType t, std::string const& text,
 
   displayMessage(t, msg);
 
+#ifndef CMAKE_BOOTSTRAP
+  // Add message to SARIF logs
+  this->SarifLog.LogMessage(t, text, backtrace);
+#endif
+
 #ifdef CMake_ENABLE_DEBUGGER
   if (DebuggerAdapter) {
     DebuggerAdapter->OnMessageOutput(t, msg.str());

+ 12 - 0
Source/cmMessenger.h

@@ -13,6 +13,10 @@
 #include "cmListFileCache.h"
 #include "cmMessageType.h" // IWYU pragma: keep
 
+#ifndef CMAKE_BOOTSTRAP
+#  include "cmSarifLog.h"
+#endif
+
 #ifdef CMake_ENABLE_DEBUGGER
 namespace cmDebugger {
 class cmDebuggerAdapter;
@@ -59,6 +63,10 @@ public:
     return this->DeprecatedWarningsAsErrors;
   }
 
+#ifndef CMAKE_BOOTSTRAP
+  cmSarif::ResultsLog const& GetSarifResultsLog() const { return SarifLog; }
+#endif
+
   // Print the top of a backtrace.
   void PrintBacktraceTitle(std::ostream& out,
                            cmListFileBacktrace const& bt) const;
@@ -76,6 +84,10 @@ private:
 
   cm::optional<std::string> TopSource;
 
+#ifndef CMAKE_BOOTSTRAP
+  cmSarif::ResultsLog SarifLog;
+#endif
+
   bool SuppressDevWarnings = false;
   bool SuppressDeprecatedWarnings = false;
   bool DevWarningsAsErrors = false;

+ 383 - 0
Source/cmSarifLog.cxx

@@ -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;
+}

+ 287 - 0
Source/cmSarifLog.h

@@ -0,0 +1,287 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+    file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include <cstddef>
+#include <functional>
+#include <string>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#include <cm/filesystem>
+#include <cm/optional>
+
+#include <cm3p/json/value.h>
+
+class cmake;
+class cmListFileBacktrace;
+enum class MessageType;
+
+/// @brief CMake support for SARIF logging
+namespace cmSarif {
+
+constexpr char const* PROJECT_SARIF_FILE_VARIABLE = "CMAKE_EXPORT_SARIF";
+
+constexpr char const* PROJECT_DEFAULT_SARIF_FILE = ".cmake/sarif/cmake.sarif";
+
+/// @brief The severity level of a result in SARIF
+///
+/// The SARIF specification section 3.27.10 defines four levels of severity
+/// for results.
+enum class ResultSeverityLevel
+{
+  SARIF_WARNING,
+  SARIF_ERROR,
+  SARIF_NOTE,
+  SARIF_NONE,
+};
+
+/// @brief A location in a source file logged with a SARIF result
+struct SourceFileLocation
+{
+  std::string Uri;
+  long Line = 0;
+
+  /// @brief Construct a SourceFileLocation at the top of the call stack
+  SourceFileLocation(cmListFileBacktrace const& backtrace);
+
+  /// @brief Get the SourceFileLocation from the top of a call stack, if any
+  /// @return The location or nullopt if the call stack is empty or is missing
+  /// location information
+  static cm::optional<SourceFileLocation> FromBacktrace(
+    cmListFileBacktrace const& backtrace);
+};
+
+/// @brief A result defined by SARIF reported by a CMake run
+///
+/// This is the data model for results in a SARIF log. Typically, a result only
+/// requires either a message or a rule index. The most common properties are
+/// named in this struct, but arbitrary metadata can be added to the result
+/// using the additionalProperties field.
+struct Result
+{
+  /// @brief The message text of the result (required if no rule index)
+  cm::optional<std::string> Message;
+
+  /// @brief The location of the result (optional)
+  cm::optional<cmSarif::SourceFileLocation> Location;
+
+  /// @brief The severity level of the result (optional)
+  cm::optional<cmSarif::ResultSeverityLevel> Level;
+
+  /// @brief The rule ID of the result (optional)
+  cm::optional<std::string> RuleId;
+
+  /// @brief The index of the rule in the log's rule array (optional)
+  cm::optional<std::size_t> RuleIndex;
+
+  /// @brief Additional JSON properties for the result (optional)
+  ///
+  /// The additional properties should be merged into the result object when it
+  /// is written to the SARIF log.
+  Json::Value AdditionalProperties;
+};
+
+/// @brief A SARIF reporting rule
+///
+/// A rule in SARIF is described by a reportingDescriptor object (SARIF
+/// specification section 3.49). The only property required for a rule is the
+/// ID property. The ID is normally an opaque string that identifies a rule
+/// applicable to a class of results. The other included properties are
+/// optional but recommended for rules reported by CMake.
+struct Rule
+{
+  /// @brief The ID of the rule. Required by SARIF
+  std::string Id;
+
+  /// @brief The end-user name of the rule (optional)
+  cm::optional<std::string> Name;
+
+  /// @brief The extended description of the rule (optional)
+  cm::optional<std::string> FullDescription;
+
+  /// @brief The default message for the rule (optional)
+  cm::optional<std::string> DefaultMessage;
+
+  /// @brief Get the JSON representation of this rule
+  Json::Value GetJson() const;
+};
+
+/// @brief A builder for SARIF rules
+///
+/// `Rule` is a data model for SARIF rules. Known rules are usually initialized
+/// manually by field. Using a builder makes initialization more readable and
+/// prevents issues with reordering and optional fields.
+class RuleBuilder
+{
+public:
+  /// @brief Construct a new rule builder for a rule with the given ID
+  RuleBuilder(char const* id) { this->NewRule.Id = id; }
+
+  /// @brief Set the name of the rule
+  RuleBuilder& Name(std::string name)
+  {
+    this->NewRule.Name = std::move(name);
+    return *this;
+  }
+
+  /// @brief Set the full description of the rule
+  RuleBuilder& FullDescription(std::string fullDescription)
+  {
+    this->NewRule.FullDescription = std::move(fullDescription);
+    return *this;
+  }
+
+  /// @brief Set the default message for the rule
+  RuleBuilder& DefaultMessage(std::string defaultMessage)
+  {
+    this->NewRule.DefaultMessage = std::move(defaultMessage);
+    return *this;
+  }
+
+  /// @brief Build the rule
+  std::pair<std::string, Rule> Build() const
+  {
+    return std::make_pair(this->NewRule.Id, this->NewRule);
+  }
+
+private:
+  Rule NewRule;
+};
+
+/// @brief Get the SARIF severity level of a CMake message type
+ResultSeverityLevel MessageSeverityLevel(MessageType t);
+
+/// @brief Get the SARIF rule ID of a CMake message type
+/// @return The rule ID or nullopt if the message type is unrecognized
+///
+/// The rule ID is a string assigned to SARIF results to identify the category
+/// of the result. CMake maps messages to rules based on the message type.
+/// CMake's rules are of the form "CMake.<MessageType>".
+cm::optional<std::string> MessageRuleId(MessageType t);
+
+/// @brief A log for reporting results in the SARIF format
+class ResultsLog
+{
+public:
+  ResultsLog();
+
+  /// @brief Log a result of this run to the SARIF output
+  void Log(cmSarif::Result&& result) const;
+
+  /// @brief Log a result from a CMake message with a source file location
+  /// @param t The type of the message, which corresponds to the level and rule
+  /// of the result
+  /// @param text The contents of the message
+  /// @param backtrace The call stack where the message originated (may be
+  /// empty)
+  void LogMessage(MessageType t, std::string const& text,
+                  cmListFileBacktrace const& backtrace) const;
+
+  /// @brief Write this SARIF log to an empty JSON object
+  /// @param[out] root The JSON object to write to
+  void WriteJson(Json::Value& root) const;
+
+private:
+  // Private methods
+
+  // Log that a rule was used and should be included in the output. Returns the
+  // index of the rule in the log
+  std::size_t UseRule(std::string const& id) const;
+
+  // Private data
+  // All data is mutable since log results are often added in const methods
+
+  // All results added chronologically
+  mutable std::vector<cmSarif::Result> Results;
+
+  // Mapping of rule IDs to rule indices in the log.
+  // In SARIF, rule metadata is typically only included if the rule is
+  // referenced. The indices are unique to one log output and and vary
+  // depending on when the rule was first encountered.
+  mutable std::unordered_map<std::string, std::size_t> RuleToIndex;
+
+  // Rules that will be added to the log in order of appearance
+  mutable std::vector<std::string> EnabledRules;
+
+  // All known rules that could be included in a log
+  mutable std::unordered_map<std::string, Rule> KnownRules;
+};
+
+/// @brief Writes contents of a `cmSarif::ResultsLog` to a file
+///
+/// The log file writer is a helper class that writes the contents of a
+/// `cmSarif::ResultsLog` upon destruction if a condition (e.g. project
+/// variable is enabled) is met.
+class LogFileWriter
+{
+public:
+  /// @brief Create a new, disabled log file writer
+  ///
+  /// The returned writer will not write anything until the path generator
+  /// and write condition are set. If the log has not been written when the
+  /// object is being destroyed, the destructor will write the log if the
+  /// condition is met and a valid path is available.
+  LogFileWriter(ResultsLog const& log)
+    : Log(log)
+  {
+  }
+
+  /// @brief Configure a log file writer for a CMake run
+  ///
+  /// CMake should write a SARIF log if the project variable
+  /// `CMAKE_EXPORT_SARIF` is `ON` or if the `--sarif-output=<path>` command
+  /// line option is set. The writer will be configured to respond to these
+  /// conditions.
+  ///
+  /// This does not configure a default path, so one must be set once it is
+  /// known that we're in normal mode if none was explicitly provided.
+  bool ConfigureForCMakeRun(cmake& cm);
+
+  ~LogFileWriter();
+
+  /// @brief Check if a valid path is set by opening the output file
+  /// @return True if the file can be opened for writing
+  bool EnsureFileValid();
+
+  /// @brief The possible outcomes of trying to write the log file
+  enum class WriteResult
+  {
+    SUCCESS, ///< File written with no issues
+    FAILURE, ///< Error encountered while writing the file
+    SKIPPED, ///< Writing was skipped due to false write condition
+  };
+
+  /// @brief Try to write the log file and return `true` if it was written
+  ///
+  /// Check the write condition and path generator to determine if the log
+  /// file should be written.
+  WriteResult TryWrite();
+
+  /// @brief Set a lambda to check if the log file should be written
+  void SetWriteCondition(std::function<bool()> const& checkConditionCallback)
+  {
+    this->WriteCondition = checkConditionCallback;
+  }
+
+  /// @brief Set the output file path, optionally creating parent directories
+  ///
+  /// The settings will apply when the log file is written. If the output
+  /// file should be checked earlier, use `CheckFileValidity`.
+  void SetPath(cm::filesystem::path const& path,
+               bool createParentDirectories = false)
+  {
+    this->FilePath = path;
+    this->CreateDirectories = createParentDirectories;
+  }
+
+private:
+  ResultsLog const& Log;
+  std::function<bool()> WriteCondition;
+  cm::filesystem::path FilePath;
+  bool CreateDirectories = false;
+  bool FileWritten = false;
+};
+
+} // namespace cmSarif

+ 39 - 0
Source/cmake.cxx

@@ -14,6 +14,7 @@
 #include <stdexcept>
 #include <utility>
 
+#include <cm/filesystem>
 #include <cm/memory>
 #include <cm/optional>
 #include <cm/string_view>
@@ -61,6 +62,9 @@
 #include "cmJSONState.h"
 #include "cmList.h"
 #include "cmMessenger.h"
+#ifndef CMAKE_BOOTSTRAP
+#  include "cmSarifLog.h"
+#endif
 #include "cmState.h"
 #include "cmStateDirectory.h"
 #include "cmStringAlgorithms.h"
@@ -1272,6 +1276,16 @@ void cmake::SetArgs(std::vector<std::string> const& args)
         state->SetIgnoreLinkWarningAsError(true);
         return true;
       } },
+#ifndef CMAKE_BOOTSTRAP
+    CommandArgument{ "--sarif-output", "No file specified for --sarif-output",
+                     CommandArgument::Values::One,
+                     [](std::string const& value, cmake* state) -> bool {
+                       state->SarifFilePath =
+                         cmSystemTools::ToNormalizedPathOnDisk(value);
+                       state->SarifFileOutput = true;
+                       return true;
+                     } },
+#endif
     CommandArgument{ "--debugger", CommandArgument::Values::Zero,
                      [](std::string const&, cmake* state) -> bool {
 #ifdef CMake_ENABLE_DEBUGGER
@@ -2853,6 +2867,15 @@ int cmake::Run(std::vector<std::string> const& args, bool noconfigure)
     return 0;
   }
 
+#ifndef CMAKE_BOOTSTRAP
+  // Configure the SARIF log for the current run
+  cmSarif::LogFileWriter sarifLogFileWriter(
+    this->GetMessenger()->GetSarifResultsLog());
+  if (!sarifLogFileWriter.ConfigureForCMakeRun(*this)) {
+    return -1;
+  }
+#endif
+
   // Log the trace format version to the desired output
   if (this->GetTrace()) {
     this->PrintTraceFormatVersion();
@@ -2879,6 +2902,17 @@ int cmake::Run(std::vector<std::string> const& args, bool noconfigure)
       cmSystemTools::Error("Error executing cmake::LoadCache(). Aborting.\n");
       return -1;
     }
+#ifndef CMAKE_BOOTSTRAP
+    // If no SARIF file has been explicitly specified, use the default path
+    if (!this->SarifFileOutput) {
+      // If no output file is specified, use the default path
+      // Enable parent directory creation for the default path
+      sarifLogFileWriter.SetPath(
+        cm::filesystem::path(this->GetHomeOutputDirectory()) /
+          std::string(cmSarif::PROJECT_DEFAULT_SARIF_FILE),
+        true);
+    }
+#endif
   } else {
     if (this->FreshCache) {
       cmSystemTools::Error("--fresh allowed only when configuring a project");
@@ -2909,6 +2943,11 @@ int cmake::Run(std::vector<std::string> const& args, bool noconfigure)
     return this->HasScriptModeExitCode() ? this->GetScriptModeExitCode() : 0;
   }
 
+#ifndef CMAKE_BOOTSTRAP
+  // CMake only responds to the SARIF variable in normal mode
+  this->MarkCliAsUsed(cmSarif::PROJECT_SARIF_FILE_VARIABLE);
+#endif
+
   // If MAKEFLAGS are given in the environment, remove the environment
   // variable.  This will prevent try-compile from succeeding when it
   // should fail (if "-i" is an option).  We cannot simply test

+ 16 - 0
Source/cmake.h

@@ -29,6 +29,8 @@
 #include "cmValue.h"
 
 #if !defined(CMAKE_BOOTSTRAP)
+#  include <type_traits>
+
 #  include <cm/optional>
 
 #  include <cm3p/json/value.h>
@@ -575,6 +577,15 @@ public:
 
   cmMessenger* GetMessenger() const { return this->Messenger.get(); }
 
+#ifndef CMAKE_BOOTSTRAP
+  /// Get the SARIF file path if set manually for this run
+  cm::optional<std::string> GetSarifFilePath() const
+  {
+    return (this->SarifFileOutput ? cm::make_optional(this->SarifFilePath)
+                                  : cm::nullopt);
+  }
+#endif
+
   /**
    * Get the state of the suppression of developer (author) warnings.
    * Returns false, by default, if developer warnings should be shown, true
@@ -811,6 +822,11 @@ private:
   cmStateSnapshot CurrentSnapshot;
   std::unique_ptr<cmMessenger> Messenger;
 
+#ifndef CMAKE_BOOTSTRAP
+  bool SarifFileOutput = false;
+  std::string SarifFilePath;
+#endif
+
   std::vector<std::string> TraceOnlyThisSources;
 
   std::set<std::string> DebugFindPkgs;

+ 1 - 0
Tests/RunCMake/CMakeLists.txt

@@ -650,6 +650,7 @@ add_RunCMake_test(project_injected)
 add_RunCMake_test(property_init)
 add_RunCMake_test(DependencyProviders)
 add_RunCMake_test(return)
+add_RunCMake_test(SarifOutput)
 add_RunCMake_test(separate_arguments)
 add_RunCMake_test(set_property)
 add_RunCMake_test(string)

+ 3 - 0
Tests/RunCMake/SarifOutput/CMakeLists.txt

@@ -0,0 +1,3 @@
+cmake_minimum_required(VERSION 3.10)
+project(${RunCMake_TEST} NONE)
+include(${RunCMake_TEST}.cmake)

+ 4 - 0
Tests/RunCMake/SarifOutput/DefaultSarifOutput-check.cmake

@@ -0,0 +1,4 @@
+# By default, no SARIF file should be generated
+if (EXISTS "${RunCMake_TEST_BINARY_DIR}/.cmake/sarif/cmake.sarif")
+  message(FATAL_ERROR "SARIF file should not have been generated by default")
+endif()

+ 4 - 0
Tests/RunCMake/SarifOutput/DefaultSarifOutput-stderr.txt

@@ -0,0 +1,4 @@
+^CMake Warning at DefaultSarifOutput\.cmake:1 \(message\):
+  Example warning message
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)$

+ 1 - 0
Tests/RunCMake/SarifOutput/DefaultSarifOutput.cmake

@@ -0,0 +1 @@
+message(WARNING "Example warning message")

+ 4 - 0
Tests/RunCMake/SarifOutput/GenerateSarifResults-check.cmake

@@ -0,0 +1,4 @@
+include("${CMAKE_CURRENT_LIST_DIR}/check-sarif.cmake")
+
+check_sarif_output("${RunCMake_TEST_BINARY_DIR}/.cmake/sarif/cmake.sarif"
+  "${CMAKE_CURRENT_LIST_DIR}/GenerateSarifResults-expected.sarif")

+ 67 - 0
Tests/RunCMake/SarifOutput/GenerateSarifResults-expected.sarif

@@ -0,0 +1,67 @@
+{
+  "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.4.json",
+  "runs": [
+    {
+    "results": [
+      {
+      "level": "warning",
+      "locations": [
+        {
+        "physicalLocation": {
+          "artifactLocation": {
+          "uri": "PATH:<SOURCE_DIR>/GenerateSarifResults.cmake"
+          },
+          "region": {
+          "startLine": 2
+          }
+        }
+        }
+      ],
+      "message": {
+        "text": "Example warning message"
+      },
+      "ruleId": "CMake.Warning",
+      "ruleIndex": 0
+      },
+      {
+        "level": "warning",
+        "locations": [
+          {
+          "physicalLocation": {
+            "artifactLocation": {
+            "uri": "PATH:<SOURCE_DIR>/GenerateSarifResults.cmake"
+            },
+            "region": {
+            "startLine": 5
+            }
+          }
+          }
+        ],
+        "message": {
+          "text": "A second example warning message"
+        },
+        "ruleId": "CMake.Warning",
+        "ruleIndex": 0
+      }
+    ],
+    "tool": {
+      "driver": {
+      "name": "CMake",
+      "rules": [
+        {
+        "id": "CMake.Warning",
+        "messageStrings": {
+          "default": {
+          "text": "CMake Warning: {0}"
+          }
+        },
+        "name": "CMake Warning"
+        }
+      ],
+      "version": "<IGNORE>"
+      }
+    }
+    }
+  ],
+  "version": "2.1.0"
+}

+ 9 - 0
Tests/RunCMake/SarifOutput/GenerateSarifResults-stderr.txt

@@ -0,0 +1,9 @@
+^CMake Warning at GenerateSarifResults\.cmake:2 \(message\):
+  Example warning message
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)
++
+CMake Warning at GenerateSarifResults\.cmake:5 \(message\):
+  A second example warning message
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)$

+ 8 - 0
Tests/RunCMake/SarifOutput/GenerateSarifResults.cmake

@@ -0,0 +1,8 @@
+# Write some user messages to produce SARIF results
+message(WARNING "Example warning message")
+
+# The second warning should be logged, but the rule should not be duplicated
+message(WARNING "A second example warning message")
+
+# Status message should not be logged
+message(STATUS "Example status message")

+ 4 - 0
Tests/RunCMake/SarifOutput/ProjectFatalError-check.cmake

@@ -0,0 +1,4 @@
+include("${CMAKE_CURRENT_LIST_DIR}/check-sarif.cmake")
+
+check_sarif_output("${RunCMake_TEST_BINARY_DIR}/.cmake/sarif/cmake.sarif"
+  "${CMAKE_CURRENT_LIST_DIR}/ProjectFatalError-expected.sarif")

+ 47 - 0
Tests/RunCMake/SarifOutput/ProjectFatalError-expected.sarif

@@ -0,0 +1,47 @@
+{
+  "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.4.json",
+  "runs": [
+    {
+      "results": [
+        {
+          "level": "error",
+          "locations": [
+            {
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "PATH:<SOURCE_DIR>/ProjectFatalError.cmake"
+                },
+                "region": {
+                  "startLine": 1
+                }
+              }
+            }
+          ],
+          "message": {
+            "text": "Example error"
+          },
+          "ruleId": "CMake.FatalError",
+          "ruleIndex": 0
+        }
+      ],
+      "tool": {
+        "driver": {
+          "name": "CMake",
+          "rules": [
+            {
+              "id": "CMake.FatalError",
+              "messageStrings": {
+                "default": {
+                  "text": "CMake Error: {0}"
+                }
+              },
+              "name": "CMake Error"
+            }
+          ],
+          "version": "<IGNORE>"
+        }
+      }
+    }
+  ],
+  "version": "2.1.0"
+}

+ 1 - 0
Tests/RunCMake/SarifOutput/ProjectFatalError-result.txt

@@ -0,0 +1 @@
+1

+ 4 - 0
Tests/RunCMake/SarifOutput/ProjectFatalError-stderr.txt

@@ -0,0 +1,4 @@
+^CMake Error at ProjectFatalError\.cmake:1 \(message\):
+  Example error
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)$

+ 1 - 0
Tests/RunCMake/SarifOutput/ProjectFatalError.cmake

@@ -0,0 +1 @@
+message(FATAL_ERROR "Example error")

+ 25 - 0
Tests/RunCMake/SarifOutput/RunCMakeTest.cmake

@@ -0,0 +1,25 @@
+include(RunCMake)
+include("${CMAKE_CURRENT_LIST_DIR}/check-sarif.cmake")
+
+# Default case: the SARIF file should not be generated
+run_cmake(DefaultSarifOutput)
+
+# Ensure the expected messages are present in the SARIF output
+run_cmake_with_options(GenerateSarifResults -DCMAKE_EXPORT_SARIF=ON)
+
+# Activate SARIF output using the `CMAKE_EXPORT_SARIF` variable
+run_cmake(ToggleExportSarifVariable)
+
+# If CMake stops with a fatal error, it should still generate a SARIF file if
+# requested (and the fatal error should be in the log)
+run_cmake_with_options(ProjectFatalError -DCMAKE_EXPORT_SARIF=ON)
+
+# ScriptModeSarifVariable Test: Script mode must ignore the
+# `CMAKE_EXPORT_SARIF`variable
+run_cmake_script(ScriptModeSarifVariable -DCMAKE_EXPORT_SARIF=ON)
+
+# Check that the command-line option can be used to set the file output path
+run_cmake_with_options(SarifFileArgument --sarif-output=test_cmake_run.sarif)
+
+# Test the command-line option in script mode as well
+run_cmake_script(SarifFileArgumentScript --sarif-output=test_cmake_run.sarif)

+ 4 - 0
Tests/RunCMake/SarifOutput/SarifFileArgument-check.cmake

@@ -0,0 +1,4 @@
+# Make sure the output exists
+if (NOT EXISTS "${RunCMake_TEST_BINARY_DIR}/test_cmake_run.sarif")
+  message(FATAL_ERROR "SARIF file not generated in the expected location")
+endif()

+ 4 - 0
Tests/RunCMake/SarifOutput/SarifFileArgument-stderr.txt

@@ -0,0 +1,4 @@
+^CMake Warning at SarifFileArgument\.cmake:1 \(message\):
+  SARIF file test
+Call Stack \(most recent call first\):
+  CMakeLists\.txt:[0-9]+ \(include\)$

+ 1 - 0
Tests/RunCMake/SarifOutput/SarifFileArgument.cmake

@@ -0,0 +1 @@
+message(WARNING "SARIF file test")

+ 4 - 0
Tests/RunCMake/SarifOutput/SarifFileArgumentScript-check.cmake

@@ -0,0 +1,4 @@
+# Make sure the output exists
+if (NOT EXISTS "${RunCMake_TEST_BINARY_DIR}/test_cmake_run.sarif")
+  message(FATAL_ERROR "SARIF file not generated in the expected location")
+endif()

+ 2 - 0
Tests/RunCMake/SarifOutput/SarifFileArgumentScript.cmake

@@ -0,0 +1,2 @@
+# This won't appear in the SARIF log, but it gives the script something to do.
+message(STATUS "SARIF file test")

+ 4 - 0
Tests/RunCMake/SarifOutput/ScriptModeSarifVariable-check.cmake

@@ -0,0 +1,4 @@
+# Script mode should ignore the SARIF project variable and export nothing
+if (EXISTS "${RunCMake_TEST_BINARY_DIR}/.cmake/sarif/cmake.sarif")
+  message(FATAL_ERROR "SARIF file should not have been generated in script mode")
+endif()

+ 3 - 0
Tests/RunCMake/SarifOutput/ScriptModeSarifVariable.cmake

@@ -0,0 +1,3 @@
+# Try enabling SARIF output in script mode
+# No file should be generated since script mode ignores the variable
+set(CMAKE_EXPORT_SARIF ON CACHE BOOL "Export SARIF results" FORCE)

+ 5 - 0
Tests/RunCMake/SarifOutput/ToggleExportSarifVariable-check.cmake

@@ -0,0 +1,5 @@
+include("${CMAKE_CURRENT_LIST_DIR}/check-sarif.cmake")
+
+# This test should produce the same output as GenerateSarifResults
+check_sarif_output("${RunCMake_TEST_BINARY_DIR}/.cmake/sarif/cmake.sarif"
+  "${CMAKE_CURRENT_LIST_DIR}/GenerateSarifResults-expected.sarif")

+ 11 - 0
Tests/RunCMake/SarifOutput/ToggleExportSarifVariable-stderr.txt

@@ -0,0 +1,11 @@
+^CMake Warning at GenerateSarifResults\.cmake:2 \(message\):
+  Example warning message
+Call Stack \(most recent call first\):
+  ToggleExportSarifVariable\.cmake:[0-9]+ \(include\)
+  CMakeLists\.txt:[0-9]+ \(include\)
++
+CMake Warning at GenerateSarifResults.cmake:5 \(message\):
+  A second example warning message
+Call Stack \(most recent call first\):
+  ToggleExportSarifVariable\.cmake:[0-9]+ \(include\)
+  CMakeLists\.txt:[0-9]+ \(include\)$

+ 6 - 0
Tests/RunCMake/SarifOutput/ToggleExportSarifVariable.cmake

@@ -0,0 +1,6 @@
+# Generate potential SARIF results
+include("${CMAKE_CURRENT_LIST_DIR}/GenerateSarifResults.cmake")
+
+# Enable SARIF logging at the end for the most behavior coverage
+# All results should be captured regardless of when enabled
+set(CMAKE_EXPORT_SARIF ON CACHE BOOL "Export SARIF results" FORCE)

+ 19 - 0
Tests/RunCMake/SarifOutput/check-sarif.cmake

@@ -0,0 +1,19 @@
+include("${CMAKE_CURRENT_LIST_DIR}/../CXXModules/check-json.cmake")
+
+# Check that the SARIF results from a test match the expected results
+macro(check_sarif_output sarif_output_file expected_sarif_output_file)
+  # Make sure the output file exists before reading it
+  if (NOT EXISTS "${sarif_output_file}")
+    message(FATAL_ERROR "SARIF output file not found: ${sarif_output_file}")
+  endif()
+  file(READ "${sarif_output_file}" actual_output)
+
+  # Make sure the expected output file exists before reading it
+  if (NOT EXISTS "${expected_sarif_output_file}")
+    message(FATAL_ERROR "Expected SARIF output file not found: ${expected_sarif_output_file}")
+  endif()
+  file(READ "${expected_sarif_output_file}" expected_output)
+
+  # Check the actual output against the expected output
+  check_json("${actual_output}" "${expected_output}")
+endmacro()