| /* 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 "cmsys/String.h" |
| |
| #include "cmDiagnostics.h" |
| #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 }; |
| } |
| |
| std::string RuleIdForMessageType(MessageType type, |
| cmDiagnosticCategory category) |
| { |
| cm::string_view name = cmDiagnostics::GetCategoryString(category); |
| if (!name.empty()) { |
| // Strip the "CMD_" prefix from the category name and convert to PascalCase |
| std::string sarifIdName; |
| bool nextWord = true; |
| for (char c : name.substr(4)) { |
| if (c == '_') { |
| nextWord = true; |
| continue; |
| } |
| if (nextWord) { |
| sarifIdName += c; |
| nextWord = false; |
| } else { |
| sarifIdName += cmsysString_tolower(c); |
| } |
| } |
| return cmStrCat("CMake.", sarifIdName); |
| } |
| |
| // Fall back to message type if not a diagnostic |
| switch (type) { |
| case MessageType::FATAL_ERROR: |
| return "CMake.FatalError"; |
| case MessageType::INTERNAL_ERROR: |
| return "CMake.InternalError"; |
| case MessageType::WARNING: |
| return "CMake.Warning"; |
| default: |
| return ""; |
| } |
| } |
| |
| cm::string_view NameForMessageType(MessageType type, |
| cmDiagnosticCategory category) |
| { |
| if (category != cmDiagnostics::CMD_NONE) { |
| return cmDiagnostics::GetCategoryString(category); |
| } |
| switch (type) { |
| case MessageType::FATAL_ERROR: |
| return "CMake Error"; |
| case MessageType::INTERNAL_ERROR: |
| return "CMake Internal Error"; |
| case MessageType::WARNING: |
| return "CMake Warning"; |
| default: |
| return ""; |
| } |
| } |
| |
| cmSarif::ReportingDescriptor ReportingDescriptorForMessageType( |
| MessageType type, cmDiagnosticCategory category) |
| { |
| cmSarif::ReportingDescriptor rd; |
| rd.Id = RuleIdForMessageType(type, category); |
| rd.Name = NameForMessageType(type, category); |
| return rd; |
| }; |
| |
| 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; |
| } |
| } |
| |
| } // 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<std::string, std::size_t> ruleIndices; |
| auto use_rule = [&](MessageType type, cmDiagnosticCategory category) { |
| std::string category_name = RuleIdForMessageType(type, category); |
| auto result = ruleIndices.emplace(category_name, 0); |
| if (result.second) { |
| result.first->second = run.Tool.Driver.Rules.size(); |
| run.Tool.Driver.Rules.emplace_back( |
| ReportingDescriptorForMessageType(type, category)); |
| } |
| return *result.first; |
| }; |
| |
| cmMessenger const& messenger = *this->CM.GetMessenger(); |
| for (auto const& message : messenger.GetDisplayedMessages()) { |
| // SARIF should only emit diagnostic messages, not general messages/logs |
| switch (message.Type) { |
| case MessageType::MESSAGE: |
| case MessageType::LOG: |
| case MessageType::UNDEFINED: |
| continue; |
| default: |
| break; |
| } |
| |
| std::pair<std::string, std::size_t> ruleInfo = |
| use_rule(message.Type, message.Category); |
| |
| 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)); |
| } |
| } |