/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying file Copyright.txt or https://cmake.org/licensing for details. */ #pragma once #include #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 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(cm::filesystem::path const& path, bool createParentDirectories = false) { this->FilePath = path; this->CreateDirectories = createParentDirectories; } private: ResultsLog const& Log; std::function WriteCondition; cm::filesystem::path FilePath; bool CreateDirectories = false; bool FileWritten = false; }; } // namespace cmSarif