cmSarif: Improve conformance with SARIF in CMake result reporting

Reorganize CMake's SARIF implementation to clearly distinguish between the SARIF standard and CMake's result reporting structure, fixing several validation failures.
- Removed unused message strings in reported rules.
- Report artifact locations relative to the CMake home directory.
- Use the recommended schema URL.
- Stop reporting some invalid locations read from the top of the stack.

cmMessenger no longer manages a SARIF log but stores a list of displayed messages. The SARIF log is constructed from displayed messages upon writing the file.
This commit is contained in:
Daniel Tierney
2026-06-29 14:12:31 -04:00
parent 4933a2de10
commit 4cc948ad08
12 changed files with 616 additions and 759 deletions
+4 -2
View File
@@ -138,6 +138,8 @@ add_library(
cmCMakePresetsGraphReadJSONTestPresets.cxx
cmCMakePresetsGraphReadJSONWorkflowPresets.cxx
cmCMakePresetsGraphResolve.cxx
cmCMakeSarifLogger.h
cmCMakeSarifLogger.cxx
cmCMakeString.hxx
cmCMakeString.cxx
cmCommandLineArgument.h
@@ -485,8 +487,8 @@ add_library(
cmRST.h
cmRuntimeDependencyArchive.cxx
cmRuntimeDependencyArchive.h
cmSarifLog.cxx
cmSarifLog.h
cmSarif.cxx
cmSarif.h
cmScriptGenerator.h
cmScriptGenerator.cxx
cmSourceFile.cxx
+221
View File
@@ -0,0 +1,221 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#include "cmCMakeSarifLogger.h"
#include <cstddef>
#include <limits>
#include <unordered_map>
#include <utility>
#include <vector>
#include <cm/string_view>
#include "cmsys/FStream.hxx"
#include "cmListFileCache.h"
#include "cmMessageType.h"
#include "cmMessenger.h"
#include "cmSarif.h"
#include "cmState.h"
#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"
#include "cmValue.h"
#include "cmVersionConfig.h"
#include "cmake.h"
// CMake-specific SARIF helpers
namespace {
constexpr char const* CMakeSarifOutputFlag = "CMAKE_EXPORT_SARIF";
constexpr char const* DefaultSarifFile = ".cmake/sarif/cmake.sarif";
cm::optional<cmSarif::Location> GetLocationFromBacktrace(
cmListFileBacktrace const& backtrace, cmake const& cm)
{
if (backtrace.Empty()) {
return {};
}
cmListFileContext const& lfc = backtrace.Top();
// Exclude frames with no real location: negative lines are deferred-call
// placeholders, and LONG_MAX is the synthetic line used by variable_watch
// callback dispatch. Neither is a meaningful source location.
if (lfc.Line < 0 || lfc.Line == std::numeric_limits<long>::max()) {
return {};
}
cmSarif::PhysicalLocation location;
location.Artifact.Uri = lfc.FilePath;
// SARIF requests that paths are given relative to a logical base. Report
// paths relative to the source dir / script working directory if possible.
location.Artifact.UriBaseId = cm.GetHomeDirectory();
std::string relative = cmSystemTools::RelativePath(
location.Artifact.UriBaseId, location.Artifact.Uri);
if (!relative.empty()) {
location.Artifact.Uri = relative;
}
if (lfc.Line != 0) {
cmSarif::Region region;
region.StartLine = lfc.Line;
location.ArtifactRegion = region;
}
return cmSarif::Location{ location };
}
cmSarif::Tool CreateCMakeTool()
{
cmSarif::ToolComponent cmDriver;
cmDriver.Name = "CMake";
cmDriver.Version = CMake_VERSION;
return cmSarif::Tool{ cmDriver };
}
cmSarif::ResultSeverityLevel SarifLevelFromMessageType(MessageType type)
{
switch (type) {
case MessageType::FATAL_ERROR:
case MessageType::INTERNAL_ERROR:
return cmSarif::ResultSeverityLevel::Error;
case MessageType::WARNING:
return cmSarif::ResultSeverityLevel::Warning;
default:
return cmSarif::ResultSeverityLevel::Note;
}
}
cm::string_view MessageRuleId(MessageType type)
{
switch (type) {
case MessageType::FATAL_ERROR:
return "CMake.FatalError";
case MessageType::INTERNAL_ERROR:
return "CMake.InternalError";
case MessageType::WARNING:
return "CMake.Warning";
case MessageType::MESSAGE:
return "CMake.Message";
case MessageType::LOG:
default:
return "CMake.Log";
}
}
cm::string_view MessageDisplayName(MessageType type)
{
switch (type) {
case MessageType::FATAL_ERROR:
return "CMake Error";
case MessageType::INTERNAL_ERROR:
return "CMake Internal Error";
case MessageType::WARNING:
return "CMake Warning";
case MessageType::MESSAGE:
return "CMake Message";
case MessageType::LOG:
default:
return "CMake Log";
}
}
cmSarif::ReportingDescriptor RuleForMessageType(MessageType type)
{
cmSarif::ReportingDescriptor rd;
rd.Id = MessageRuleId(type);
rd.Name = MessageDisplayName(type);
return rd;
}
} // namespace
cmCMakeSarifLogger::cmCMakeSarifLogger(cmake& cm)
: CM(cm)
{
if (this->CM.GetState()->GetRole() == cmState::Role::Project) {
cm.MarkCliAsUsed(CMakeSarifOutputFlag);
}
}
cmCMakeSarifLogger::~cmCMakeSarifLogger()
{
this->GenerateForRun();
}
cm::optional<std::string> cmCMakeSarifLogger::FileOutputPath() const
{
// If a SARIF path was specified via CLI, use it. Otherwise, check whether
// logging is enabled via the project cache variable and use the default
// path if so.
if (cm::optional<std::string> specifiedPath = this->CM.GetSarifFilePath()) {
return specifiedPath;
}
if (this->CM.GetState()->GetRole() == cmState::Role::Project &&
this->CM.GetCacheDefinition(CMakeSarifOutputFlag).IsOn()) {
return cmStrCat(this->CM.GetHomeOutputDirectory(), '/', DefaultSarifFile);
}
return cm::nullopt;
}
bool cmCMakeSarifLogger::WriteFile(std::string const& path,
bool createParentDirectories) const
{
if (createParentDirectories) {
if (!cmSystemTools::MakeDirectory(cmSystemTools::GetFilenamePath(path))
.IsSuccess()) {
return false;
}
}
cmsys::ofstream outputFile(path);
if (!outputFile.good()) {
return false;
}
// Run object to build
cmSarif::Run run;
run.Tool = CreateCMakeTool();
// Helper to add rules to the run as encountered in results and get their
// index for reporting
std::unordered_map<cm::string_view, std::size_t> ruleIndices;
auto use_rule = [&](MessageType t) {
cm::string_view category_name = MessageRuleId(t);
auto result = ruleIndices.emplace(category_name, 0);
if (result.second) {
result.first->second = run.Tool.Driver.Rules.size();
run.Tool.Driver.Rules.emplace_back(RuleForMessageType(t));
}
return *result.first;
};
cmMessenger const& messenger = *this->CM.GetMessenger();
for (auto const& message : messenger.GetDisplayedMessages()) {
std::pair<cm::string_view, std::size_t> ruleInfo = use_rule(message.Type);
cmSarif::Result result;
result.RuleId = ruleInfo.first;
result.RuleIndex = ruleInfo.second;
result.Message = message.Text;
result.Location = GetLocationFromBacktrace(message.Backtrace, this->CM);
result.Level = SarifLevelFromMessageType(message.Type);
run.Results.emplace_back(std::move(result));
}
return cmSarif::WriteLog(path, run);
}
void cmCMakeSarifLogger::GenerateForRun() const
{
cm::optional<std::string> path = this->FileOutputPath();
if (!path) {
return;
}
// If using the default path within the build dir, ensure parents are created
bool const createParents = !this->CM.GetSarifFilePath().has_value();
if (!this->WriteFile(*path, createParents)) {
cmSystemTools::Error(cmStrCat("Failed to write SARIF log to ", *path));
}
}
+29
View File
@@ -0,0 +1,29 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#pragma once
#include <string>
#include <cm/optional>
class cmake;
/// @brief Manages SARIF logging for a CMake run
///
/// Writes diagnostics collected during a CMake run to a SARIF log file if
/// enabled by conditions.
class cmCMakeSarifLogger final
{
public:
cmCMakeSarifLogger(cmake& cm);
~cmCMakeSarifLogger();
void GenerateForRun() const;
private:
bool WriteFile(std::string const& path,
bool createParentDirectories = false) const;
cm::optional<std::string> FileOutputPath() const;
cmake const& CM;
};
+6 -9
View File
@@ -19,8 +19,6 @@
#if !defined(CMAKE_BOOTSTRAP)
# include "cmsys/SystemInformation.hxx"
# include "cmSarifLog.h"
#endif
#ifdef CMake_ENABLE_DEBUGGER
@@ -178,7 +176,7 @@ void PrintCallStack(std::ostream& out, cmListFileBacktrace bt,
} // anonymous namespace
void cmMessenger::IssueMessage(MessageType t, std::string const& text,
cmListFileBacktrace const& backtrace) const
cmListFileBacktrace const& backtrace)
{
this->DisplayMessage(t, cmDiagnostics::CMD_NONE, text, backtrace);
}
@@ -186,7 +184,7 @@ void cmMessenger::IssueMessage(MessageType t, std::string const& text,
void cmMessenger::IssueDiagnostic(cmDiagnosticCategory category,
std::string const& text,
cmStateSnapshot const& fallbackContext,
cmDiagnosticContext const& context) const
cmDiagnosticContext const& context)
{
cmDiagnosticAction const action = [&] {
if (context.HasState) {
@@ -229,7 +227,7 @@ void cmMessenger::IssueDiagnostic(cmDiagnosticCategory category,
void cmMessenger::DisplayMessage(MessageType type,
cmDiagnosticCategory category,
std::string const& text,
cmListFileBacktrace const& backtrace) const
cmListFileBacktrace const& backtrace)
{
std::ostringstream msg;
@@ -249,10 +247,9 @@ void cmMessenger::DisplayMessage(MessageType type,
displayMessage(type, category, msg);
#ifndef CMAKE_BOOTSTRAP
// Add message to SARIF logs
this->SarifLog.LogMessage(type, text, backtrace);
#endif
// Add message to logs
this->DisplayedMessages.emplace_back(
Message{ type, category, backtrace, text });
#ifdef CMake_ENABLE_DEBUGGER
if (DebuggerAdapter) {
+18 -14
View File
@@ -7,6 +7,7 @@
#include <iosfwd>
#include <memory>
#include <string>
#include <vector>
#include <cm/optional>
@@ -15,10 +16,6 @@
#include "cmListFileCache.h"
#include "cmMessageType.h" // IWYU pragma: keep
#ifndef CMAKE_BOOTSTRAP
# include "cmSarifLog.h"
#endif
class cmStateSnapshot;
#ifdef CMake_ENABLE_DEBUGGER
@@ -32,22 +29,18 @@ class cmMessenger
public:
void IssueMessage(
MessageType type, std::string const& text,
cmListFileBacktrace const& backtrace = cmListFileBacktrace()) const;
cmListFileBacktrace const& backtrace = cmListFileBacktrace());
void IssueDiagnostic(cmDiagnosticCategory category, std::string const& text,
cmStateSnapshot const& fallbackContext,
cmDiagnosticContext const& context = {}) const;
cmDiagnosticContext const& context = {});
void DisplayMessage(MessageType type, cmDiagnosticCategory category,
std::string const& text,
cmListFileBacktrace const& backtrace) const;
cmListFileBacktrace const& backtrace);
void SetTopSource(cm::optional<std::string> topSource);
#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;
@@ -59,12 +52,23 @@ public:
}
#endif
struct Message
{
MessageType Type;
cmDiagnosticCategory Category;
cmListFileBacktrace Backtrace;
std::string Text;
};
std::vector<Message> const& GetDisplayedMessages() const
{
return this->DisplayedMessages;
}
private:
cm::optional<std::string> TopSource;
#ifndef CMAKE_BOOTSTRAP
cmSarif::ResultsLog SarifLog;
#endif
std::vector<Message> DisplayedMessages;
#ifdef CMake_ENABLE_DEBUGGER
std::shared_ptr<cmDebugger::cmDebuggerAdapter> DebuggerAdapter;
+158
View File
@@ -0,0 +1,158 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#include "cmSarif.h"
#include <memory>
#include <cm3p/json/value.h>
#include <cm3p/json/writer.h>
#include "cmsys/FStream.hxx"
namespace cmSarif {
constexpr char const* SpecVersion = "2.1.0";
constexpr char const* SpecSchema =
"https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/"
"sarif-schema-2.1.0.json";
Json::Value GetJson(ResultSeverityLevel level)
{
switch (level) {
case ResultSeverityLevel::Warning:
return "warning";
case ResultSeverityLevel::Error:
return "error";
case ResultSeverityLevel::Note:
return "note";
case ResultSeverityLevel::None:
default:
return "none";
}
}
Json::Value GetJson(ArtifactLocation const& artifactLocation)
{
Json::Value obj(Json::objectValue);
obj["uri"] = artifactLocation.Uri;
if (!artifactLocation.UriBaseId.empty()) {
obj["uriBaseId"] = artifactLocation.UriBaseId;
}
return obj;
}
Json::Value GetJson(Region region)
{
Json::Value obj(Json::objectValue);
obj["startLine"] = Json::Int64(region.StartLine);
return obj;
}
Json::Value GetJson(PhysicalLocation const& physicalLocation)
{
Json::Value obj(Json::objectValue);
obj["artifactLocation"] = cmSarif::GetJson(physicalLocation.Artifact);
if (physicalLocation.ArtifactRegion) {
obj["region"] = cmSarif::GetJson(*physicalLocation.ArtifactRegion);
}
return obj;
}
Json::Value GetJson(Location const& location)
{
Json::Value obj(Json::objectValue);
obj["physicalLocation"] = cmSarif::GetJson(location.Physical);
return obj;
}
Json::Value GetJson(ReportingDescriptor const& reportingDescriptor)
{
Json::Value rd(Json::objectValue);
rd["id"] = reportingDescriptor.Id;
if (reportingDescriptor.Name) {
rd["name"] = *reportingDescriptor.Name;
}
return rd;
}
Json::Value GetJson(Result const& result)
{
Json::Value resultJson(Json::objectValue);
if (result.Message) {
resultJson["message"]["text"] = *result.Message;
}
if (result.Level) {
resultJson["level"] = cmSarif::GetJson(*result.Level);
}
if (result.RuleId) {
resultJson["ruleId"] = *result.RuleId;
}
if (result.RuleIndex) {
resultJson["ruleIndex"] = Json::UInt64(*result.RuleIndex);
}
if (result.Location) {
resultJson["locations"][0] = cmSarif::GetJson(*result.Location);
}
return resultJson;
}
Json::Value GetJson(ToolComponent const& toolComponent)
{
Json::Value component(Json::objectValue);
component["name"] = toolComponent.Name;
component["version"] = toolComponent.Version;
Json::Value rules(Json::arrayValue);
for (auto const& rule : toolComponent.Rules) {
rules.append(cmSarif::GetJson(rule));
}
component["rules"] = rules;
return component;
}
Json::Value GetJson(Tool const& tool)
{
Json::Value toolJson(Json::objectValue);
toolJson["driver"] = cmSarif::GetJson(tool.Driver);
return toolJson;
}
Json::Value GetJson(Run const& run)
{
Json::Value runJson(Json::objectValue);
runJson["tool"] = cmSarif::GetJson(run.Tool);
Json::Value results(Json::arrayValue);
for (auto const& result : run.Results) {
results.append(cmSarif::GetJson(result));
}
runJson["results"] = results;
return runJson;
}
bool WriteLog(std::string const& path, cmSarif::Run const& run)
{
cmsys::ofstream outputFile(path.c_str());
if (!outputFile.good()) {
return false;
}
Json::Value root(Json::objectValue);
root["version"] = SpecVersion;
root["$schema"] = SpecSchema;
Json::Value runs(Json::arrayValue);
runs.append(cmSarif::GetJson(run));
root["runs"] = runs;
Json::StreamWriterBuilder builder;
std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
writer->write(root, &outputFile);
outputFile.close();
return true;
}
} // namespace cmSarif
+125
View File
@@ -0,0 +1,125 @@
#pragma once
#include <cstddef>
#include <string>
#include <vector>
#include <cm/optional>
#include <cm3p/json/value.h>
/// @brief Objects for building serializable SARIF logs
namespace cmSarif {
/// @brief The severity level of a result in SARIF
///
/// The SARIF specification section 3.27.10 defines four levels of severity
/// for results. It is a string property of a result rather than its own type.
enum class ResultSeverityLevel
{
Warning,
Error,
Note,
None,
};
Json::Value GetJson(ResultSeverityLevel level);
/// @brief SARIF artifactLocation object (§3.4)
struct ArtifactLocation
{
std::string Uri;
std::string UriBaseId;
};
Json::Value GetJson(ArtifactLocation const& artifactLocation);
/// @brief SARIF region object (§3.30)
struct Region
{
long StartLine;
};
Json::Value GetJson(Region region);
/// @brief SARIF physicalLocation object (§3.29)
struct PhysicalLocation
{
ArtifactLocation Artifact;
cm::optional<Region> ArtifactRegion;
};
Json::Value GetJson(PhysicalLocation const& physicalLocation);
/// @brief SARIF location object (§3.28)
struct Location
{
PhysicalLocation Physical;
};
Json::Value GetJson(Location const& location);
/// @brief A result reported by a run of a static analysis tool
///
/// This is the data model for results in a SARIF log. Typically, a result only
/// requires either a message or a rule index.
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::Location> 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;
};
Json::Value GetJson(Result const& result);
/// @brief A reporting descriptor provides information about an analysis result
///
/// Reporting descriptors (SARIF specification section 3.49) provide
/// information about categories of reporting items and is used to define
/// rules and taxa.
struct ReportingDescriptor
{
std::string Id;
cm::optional<std::string> Name;
};
Json::Value GetJson(ReportingDescriptor const& reportingDescriptor);
struct ToolComponent
{
std::string Name;
std::string Version;
std::vector<ReportingDescriptor> Rules;
};
Json::Value GetJson(ToolComponent const& toolComponent);
struct Tool
{
ToolComponent Driver;
};
Json::Value GetJson(Tool const& tool);
struct Run
{
cmSarif::Tool Tool;
std::vector<Result> Results;
};
Json::Value GetJson(Run const& run);
bool WriteLog(std::string const& path, cmSarif::Run const& run);
} // namespace cmSarif
-368
View File
@@ -1,368 +0,0 @@
/* 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 <cm3p/json/value.h>
#include <cm3p/json/writer.h>
#include "cmsys/FStream.hxx"
#include "cmListFileCache.h"
#include "cmMessageType.h"
#include "cmState.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::WARNING:
return ResultSeverityLevel::SARIF_WARNING;
case MessageType::FATAL_ERROR:
case MessageType::INTERNAL_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::WARNING:
return "CMake.Warning";
case MessageType::FATAL_ERROR:
return "CMake.FatalError";
case MessageType::INTERNAL_ERROR:
return "CMake.InternalError";
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);
}
}
}
bool cmSarif::LogFileWriter::EnsureFileValid()
{
// First, ensure directory exists
std::string const dir = cmSystemTools::GetFilenamePath(this->FilePath);
if (!cmSystemTools::FileIsDirectory(dir)) {
if (!this->CreateDirectories ||
!cmSystemTools::MakeDirectory(dir).IsSuccess()) {
return false;
}
}
// Open the file for writing
cmsys::ofstream outputFile(this->FilePath.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.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
if (cm::optional<std::string> sarifFilePath = cm.GetSarifFilePath()) {
this->SetPath(*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.GetState()->GetRole() == cmState::Role::Project &&
cm.GetCacheDefinition(cmSarif::PROJECT_SARIF_FILE_VARIABLE).IsOn());
});
return true;
}
-285
View File
@@ -1,285 +0,0 @@
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
#pragma once
#include <cstddef>
#include <functional>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#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 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(std::string const& path, bool createParentDirectories = false)
{
this->FilePath = path;
this->CreateDirectories = createParentDirectories;
}
private:
ResultsLog const& Log;
std::function<bool()> WriteCondition;
std::string FilePath;
bool CreateDirectories = false;
bool FileWritten = false;
};
} // namespace cmSarif
+2 -21
View File
@@ -84,6 +84,7 @@
# include <cm3p/json/writer.h>
# include "cmCMakePresetsArgs.h"
# include "cmCMakeSarifLogger.h"
# include "cmConfigureLog.h"
# include "cmFileAPI.h"
# include "cmGraphVizWriter.h"
@@ -91,7 +92,6 @@
# include "cmInstrumentationInterrupt.h"
# include "cmInstrumentationQuery.h"
# include "cmMakefileProfilingData.h"
# include "cmSarifLog.h"
# include "cmVariableWatch.h"
#endif
@@ -3076,11 +3076,7 @@ int cmake::Run(std::vector<std::string> const& args, bool noconfigure)
#ifndef CMAKE_BOOTSTRAP
// Configure the SARIF log for the current run
cmSarif::LogFileWriter sarifLogFileWriter(
this->GetMessenger()->GetSarifResultsLog());
if (!sarifLogFileWriter.ConfigureForCMakeRun(*this)) {
return -1;
}
cmCMakeSarifLogger sarifLogger(*this);
this->VariableWatch->AddWatch("CMAKE_WARN_DEPRECATED", cmDeprecatedWatch);
this->VariableWatch->AddWatch("CMAKE_ERROR_DEPRECATED", cmDeprecatedWatch);
@@ -3112,16 +3108,6 @@ 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(cmStrCat(this->GetHomeOutputDirectory(), '/',
cmSarif::PROJECT_DEFAULT_SARIF_FILE),
true);
}
#endif
} else {
if (this->FreshCache) {
cmSystemTools::Error("--fresh allowed only when configuring a project");
@@ -3156,11 +3142,6 @@ 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
@@ -1,67 +1,64 @@
{
"$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.4.json",
"$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json",
"runs": [
{
"results": [
{
"level": "warning",
"locations": [
"results": [
{
"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
"level": "warning",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "GenerateSarifResults.cmake",
"uriBaseId": "PATH:<SOURCE_DIR>"
},
"region": {
"startLine": 2
}
}
}
}
}
],
"message": {
"text": "A second example warning message"
],
"message": {
"text": "Example warning message"
},
"ruleId": "CMake.Warning",
"ruleIndex": 0
},
"ruleId": "CMake.Warning",
"ruleIndex": 0
}
],
"tool": {
"driver": {
"name": "CMake",
"rules": [
{
"id": "CMake.Warning",
"messageStrings": {
"default": {
"text": "CMake Warning: {0}"
}
},
"name": "CMake Warning"
"level": "warning",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "GenerateSarifResults.cmake",
"uriBaseId": "PATH:<SOURCE_DIR>"
},
"region": {
"startLine": 5
}
}
}
],
"message": {
"text": "A second example warning message"
},
"ruleId": "CMake.Warning",
"ruleIndex": 0
}
],
"version": "<IGNORE>"
"tool": {
"driver": {
"name": "CMake",
"rules": [
{
"id": "CMake.Warning",
"name": "CMake Warning"
}
],
"version": "<IGNORE>"
}
}
}
}
],
"version": "2.1.0"
}
@@ -1,5 +1,5 @@
{
"$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.4.json",
"$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json",
"runs": [
{
"results": [
@@ -9,7 +9,8 @@
{
"physicalLocation": {
"artifactLocation": {
"uri": "PATH:<SOURCE_DIR>/ProjectFatalError.cmake"
"uri": "ProjectFatalError.cmake",
"uriBaseId": "PATH:<SOURCE_DIR>"
},
"region": {
"startLine": 1
@@ -30,11 +31,6 @@
"rules": [
{
"id": "CMake.FatalError",
"messageStrings": {
"default": {
"text": "CMake Error: {0}"
}
},
"name": "CMake Error"
}
],