diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 91bc133897..3434183430 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -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 diff --git a/Source/cmCMakeSarifLogger.cxx b/Source/cmCMakeSarifLogger.cxx new file mode 100644 index 0000000000..ed20ef4474 --- /dev/null +++ b/Source/cmCMakeSarifLogger.cxx @@ -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 +#include +#include +#include +#include + +#include + +#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 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::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 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 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 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 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 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)); + } +} diff --git a/Source/cmCMakeSarifLogger.h b/Source/cmCMakeSarifLogger.h new file mode 100644 index 0000000000..4c76962c49 --- /dev/null +++ b/Source/cmCMakeSarifLogger.h @@ -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 + +#include + +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 FileOutputPath() const; + + cmake const& CM; +}; diff --git a/Source/cmMessenger.cxx b/Source/cmMessenger.cxx index 12176baba8..3aebc92b80 100644 --- a/Source/cmMessenger.cxx +++ b/Source/cmMessenger.cxx @@ -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) { diff --git a/Source/cmMessenger.h b/Source/cmMessenger.h index 60f736b864..3ae7fc013c 100644 --- a/Source/cmMessenger.h +++ b/Source/cmMessenger.h @@ -7,6 +7,7 @@ #include #include #include +#include #include @@ -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 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 const& GetDisplayedMessages() const + { + return this->DisplayedMessages; + } + private: cm::optional TopSource; -#ifndef CMAKE_BOOTSTRAP - cmSarif::ResultsLog SarifLog; -#endif + std::vector DisplayedMessages; #ifdef CMake_ENABLE_DEBUGGER std::shared_ptr DebuggerAdapter; diff --git a/Source/cmSarif.cxx b/Source/cmSarif.cxx new file mode 100644 index 0000000000..7b711d8821 --- /dev/null +++ b/Source/cmSarif.cxx @@ -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 + +#include +#include + +#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 writer(builder.newStreamWriter()); + writer->write(root, &outputFile); + outputFile.close(); + + return true; +} + +} // namespace cmSarif diff --git a/Source/cmSarif.h b/Source/cmSarif.h new file mode 100644 index 0000000000..b0883d633f --- /dev/null +++ b/Source/cmSarif.h @@ -0,0 +1,125 @@ +#pragma once + +#include +#include +#include + +#include + +#include + +/// @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 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 Message; + + /// @brief The location of the result (optional) + cm::optional Location; + + /// @brief The severity level of the result (optional) + cm::optional Level; + + /// @brief The rule ID of the result (optional) + cm::optional RuleId; + + /// @brief The index of the rule in the log's rule array (optional) + cm::optional 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 Name; +}; + +Json::Value GetJson(ReportingDescriptor const& reportingDescriptor); + +struct ToolComponent +{ + std::string Name; + std::string Version; + std::vector Rules; +}; + +Json::Value GetJson(ToolComponent const& toolComponent); + +struct Tool +{ + ToolComponent Driver; +}; + +Json::Value GetJson(Tool const& tool); + +struct Run +{ + cmSarif::Tool Tool; + std::vector Results; +}; + +Json::Value GetJson(Run const& run); + +bool WriteLog(std::string const& path, cmSarif::Run const& run); + +} // namespace cmSarif diff --git a/Source/cmSarifLog.cxx b/Source/cmSarifLog.cxx deleted file mode 100644 index 63c00c385e..0000000000 --- a/Source/cmSarifLog.cxx +++ /dev/null @@ -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 -#include - -#include -#include - -#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 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::FromBacktrace( - cmListFileBacktrace const& backtrace) -{ - if (backtrace.Empty()) { - return cm::nullopt; - } - cmListFileContext const& lfc = backtrace.Top(); - if (lfc.Line <= 0 || lfc.FilePath.empty()) { - return cm::nullopt; - } - - return cm::make_optional(backtrace); -} - -void cmSarif::ResultsLog::WriteJson(Json::Value& root) const -{ - // Add SARIF metadata - root["version"] = "2.1.0"; - root["$schema"] = "https://schemastore.azurewebsites.net/schemas/json/" - "sarif-2.1.0-rtm.4.json"; - - // JSON object for the SARIF runs array - Json::Value runs(Json::arrayValue); - - // JSON object for the current (only) run - Json::Value currentRun(Json::objectValue); - - // Accumulate info about the reported rules - Json::Value jsonRules(Json::arrayValue); - for (auto const& ruleId : this->EnabledRules) { - jsonRules.append(KnownRules.at(ruleId).GetJson()); - } - - // Add info the driver for the current run (CMake) - Json::Value driverTool(Json::objectValue); - driverTool["name"] = "CMake"; - driverTool["version"] = CMake_VERSION; - driverTool["rules"] = jsonRules; - currentRun["tool"]["driver"] = driverTool; - - runs.append(currentRun); - - // Add all results - Json::Value jsonResults(Json::arrayValue); - for (auto const& res : this->Results) { - Json::Value jsonResult(Json::objectValue); - - if (res.Message) { - jsonResult["message"]["text"] = *(res.Message); - } - - // If the result has a level, add it to the result - if (res.Level) { - switch (*res.Level) { - case ResultSeverityLevel::SARIF_WARNING: - jsonResult["level"] = "warning"; - break; - case ResultSeverityLevel::SARIF_ERROR: - jsonResult["level"] = "error"; - break; - case ResultSeverityLevel::SARIF_NOTE: - jsonResult["level"] = "note"; - break; - case ResultSeverityLevel::SARIF_NONE: - jsonResult["level"] = "none"; - break; - } - } - - // If the result has a rule ID or index, add it to the result - if (res.RuleId) { - jsonResult["ruleId"] = *res.RuleId; - } - if (res.RuleIndex) { - jsonResult["ruleIndex"] = Json::UInt64(*res.RuleIndex); - } - - if (res.Location) { - jsonResult["locations"][0]["physicalLocation"]["artifactLocation"] - ["uri"] = (res.Location)->Uri; - jsonResult["locations"][0]["physicalLocation"]["region"]["startLine"] = - Json::Int64((res.Location)->Line); - } - - jsonResults.append(jsonResult); - } - - currentRun["results"] = jsonResults; - runs[0] = currentRun; - root["runs"] = runs; -} - -cmSarif::LogFileWriter::~LogFileWriter() -{ - // If the file has not been written yet, try to finalize it - if (!this->FileWritten) { - // Try to write and check the result - if (this->TryWrite() == WriteResult::FAILURE) { - // If the result is `FAILURE`, it means the write condition is true but - // the file still wasn't written. This is an error. - cmSystemTools::Error("Failed to write SARIF log to " + this->FilePath); - } - } -} - -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 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 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; -} diff --git a/Source/cmSarifLog.h b/Source/cmSarifLog.h deleted file mode 100644 index f7e065a389..0000000000 --- a/Source/cmSarifLog.h +++ /dev/null @@ -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 -#include -#include -#include -#include -#include - -#include - -#include - -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 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 Message; - - /// @brief The location of the result (optional) - cm::optional Location; - - /// @brief The severity level of the result (optional) - cm::optional Level; - - /// @brief The rule ID of the result (optional) - cm::optional RuleId; - - /// @brief The index of the rule in the log's rule array (optional) - cm::optional 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 Name; - - /// @brief The extended description of the rule (optional) - cm::optional FullDescription; - - /// @brief The default message for the rule (optional) - cm::optional 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 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.". -cm::optional 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 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 RuleToIndex; - - // Rules that will be added to the log in order of appearance - mutable std::vector EnabledRules; - - // All known rules that could be included in a log - mutable std::unordered_map 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=` 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 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 WriteCondition; - std::string FilePath; - bool CreateDirectories = false; - bool FileWritten = false; -}; - -} // namespace cmSarif diff --git a/Source/cmake.cxx b/Source/cmake.cxx index b9a9aa6515..80c648aef2 100644 --- a/Source/cmake.cxx +++ b/Source/cmake.cxx @@ -84,6 +84,7 @@ # include # 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 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 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 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 diff --git a/Tests/RunCMake/SarifOutput/GenerateSarifResults-expected.sarif b/Tests/RunCMake/SarifOutput/GenerateSarifResults-expected.sarif index 2a66e6ba55..8151e4ee32 100644 --- a/Tests/RunCMake/SarifOutput/GenerateSarifResults-expected.sarif +++ b/Tests/RunCMake/SarifOutput/GenerateSarifResults-expected.sarif @@ -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:/GenerateSarifResults.cmake" - }, - "region": { - "startLine": 2 - } - } - } - ], - "message": { - "text": "Example warning message" - }, - "ruleId": "CMake.Warning", - "ruleIndex": 0 - }, - { - "level": "warning", - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "PATH:/GenerateSarifResults.cmake" - }, - "region": { - "startLine": 5 + "level": "warning", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "GenerateSarifResults.cmake", + "uriBaseId": "PATH:" + }, + "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:" + }, + "region": { + "startLine": 5 + } + } + } + ], + "message": { + "text": "A second example warning message" + }, + "ruleId": "CMake.Warning", + "ruleIndex": 0 } ], - "version": "" + "tool": { + "driver": { + "name": "CMake", + "rules": [ + { + "id": "CMake.Warning", + "name": "CMake Warning" + } + ], + "version": "" + } } } - } ], "version": "2.1.0" } diff --git a/Tests/RunCMake/SarifOutput/ProjectFatalError-expected.sarif b/Tests/RunCMake/SarifOutput/ProjectFatalError-expected.sarif index f36f101ccf..0c7df3025f 100644 --- a/Tests/RunCMake/SarifOutput/ProjectFatalError-expected.sarif +++ b/Tests/RunCMake/SarifOutput/ProjectFatalError-expected.sarif @@ -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:/ProjectFatalError.cmake" + "uri": "ProjectFatalError.cmake", + "uriBaseId": "PATH:" }, "region": { "startLine": 1 @@ -30,11 +31,6 @@ "rules": [ { "id": "CMake.FatalError", - "messageStrings": { - "default": { - "text": "CMake Error: {0}" - } - }, "name": "CMake Error" } ],