cmSarifLog.cxx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
  2. file Copyright.txt or https://cmake.org/licensing for details. */
  3. #include "cmSarifLog.h"
  4. #include <memory>
  5. #include <stdexcept>
  6. #include <cm/filesystem>
  7. #include <cm3p/json/value.h>
  8. #include <cm3p/json/writer.h>
  9. #include "cmsys/FStream.hxx"
  10. #include "cmListFileCache.h"
  11. #include "cmMessageType.h"
  12. #include "cmStringAlgorithms.h"
  13. #include "cmSystemTools.h"
  14. #include "cmValue.h"
  15. #include "cmVersionConfig.h"
  16. #include "cmake.h"
  17. cmSarif::ResultsLog::ResultsLog()
  18. {
  19. // Add the known CMake rules
  20. this->KnownRules.emplace(RuleBuilder("CMake.AuthorWarning")
  21. .Name("CMake Warning (dev)")
  22. .DefaultMessage("CMake Warning (dev): {0}")
  23. .Build());
  24. this->KnownRules.emplace(RuleBuilder("CMake.Warning")
  25. .Name("CMake Warning")
  26. .DefaultMessage("CMake Warning: {0}")
  27. .Build());
  28. this->KnownRules.emplace(RuleBuilder("CMake.DeprecationWarning")
  29. .Name("CMake Deprecation Warning")
  30. .DefaultMessage("CMake Deprecation Warning: {0}")
  31. .Build());
  32. this->KnownRules.emplace(RuleBuilder("CMake.AuthorError")
  33. .Name("CMake Error (dev)")
  34. .DefaultMessage("CMake Error (dev): {0}")
  35. .Build());
  36. this->KnownRules.emplace(RuleBuilder("CMake.FatalError")
  37. .Name("CMake Error")
  38. .DefaultMessage("CMake Error: {0}")
  39. .Build());
  40. this->KnownRules.emplace(
  41. RuleBuilder("CMake.InternalError")
  42. .Name("CMake Internal Error")
  43. .DefaultMessage("CMake Internal Error (please report a bug): {0}")
  44. .Build());
  45. this->KnownRules.emplace(RuleBuilder("CMake.DeprecationError")
  46. .Name("CMake Deprecation Error")
  47. .DefaultMessage("CMake Deprecation Error: {0}")
  48. .Build());
  49. this->KnownRules.emplace(RuleBuilder("CMake.Message")
  50. .Name("CMake Message")
  51. .DefaultMessage("CMake Message: {0}")
  52. .Build());
  53. this->KnownRules.emplace(RuleBuilder("CMake.Log")
  54. .Name("CMake Log")
  55. .DefaultMessage("CMake Log: {0}")
  56. .Build());
  57. }
  58. void cmSarif::ResultsLog::Log(cmSarif::Result&& result) const
  59. {
  60. // The rule ID is optional, but if it is present, enable metadata output for
  61. // the rule by marking it as used
  62. if (result.RuleId) {
  63. std::size_t index = this->UseRule(*result.RuleId);
  64. result.RuleIndex = index;
  65. }
  66. // Add the result to the log
  67. this->Results.emplace_back(result);
  68. }
  69. void cmSarif::ResultsLog::LogMessage(
  70. MessageType t, std::string const& text,
  71. cmListFileBacktrace const& backtrace) const
  72. {
  73. // Add metadata to the result object
  74. // The CMake SARIF rules for messages all expect 1 string argument with the
  75. // message text
  76. Json::Value additionalProperties(Json::objectValue);
  77. Json::Value args(Json::arrayValue);
  78. args.append(text);
  79. additionalProperties["message"]["id"] = "default";
  80. additionalProperties["message"]["arguments"] = args;
  81. // Create and log a result object
  82. // Rule indices are assigned when writing the final JSON output. Right now,
  83. // leave it as nullopt. The other optional fields are filled if available
  84. this->Log(cmSarif::Result{
  85. text, cmSarif::SourceFileLocation::FromBacktrace(backtrace),
  86. cmSarif::MessageSeverityLevel(t), cmSarif::MessageRuleId(t), cm::nullopt,
  87. additionalProperties });
  88. }
  89. std::size_t cmSarif::ResultsLog::UseRule(std::string const& id) const
  90. {
  91. // Check if the rule is already in the index
  92. auto it = this->RuleToIndex.find(id);
  93. if (it != this->RuleToIndex.end()) {
  94. // The rule is already in use. Return the known index
  95. return it->second;
  96. }
  97. // This rule is not yet in the index, so check if it is recognized
  98. auto itKnown = this->KnownRules.find(id);
  99. if (itKnown == this->KnownRules.end()) {
  100. // The rule is not known. Add an empty rule to the known rules so that it
  101. // is included in the output
  102. this->KnownRules.emplace(RuleBuilder(id.c_str()).Build());
  103. }
  104. // Since this is the first time the rule is used, enable it and add it to the
  105. // index
  106. std::size_t idx = this->EnabledRules.size();
  107. this->RuleToIndex[id] = idx;
  108. this->EnabledRules.emplace_back(id);
  109. return idx;
  110. }
  111. cmSarif::ResultSeverityLevel cmSarif::MessageSeverityLevel(MessageType t)
  112. {
  113. switch (t) {
  114. case MessageType::AUTHOR_WARNING:
  115. case MessageType::WARNING:
  116. case MessageType::DEPRECATION_WARNING:
  117. return ResultSeverityLevel::SARIF_WARNING;
  118. case MessageType::AUTHOR_ERROR:
  119. case MessageType::FATAL_ERROR:
  120. case MessageType::INTERNAL_ERROR:
  121. case MessageType::DEPRECATION_ERROR:
  122. return ResultSeverityLevel::SARIF_ERROR;
  123. case MessageType::MESSAGE:
  124. case MessageType::LOG:
  125. return ResultSeverityLevel::SARIF_NOTE;
  126. default:
  127. return ResultSeverityLevel::SARIF_NONE;
  128. }
  129. }
  130. cm::optional<std::string> cmSarif::MessageRuleId(MessageType t)
  131. {
  132. switch (t) {
  133. case MessageType::AUTHOR_WARNING:
  134. return "CMake.AuthorWarning";
  135. case MessageType::WARNING:
  136. return "CMake.Warning";
  137. case MessageType::DEPRECATION_WARNING:
  138. return "CMake.DeprecationWarning";
  139. case MessageType::AUTHOR_ERROR:
  140. return "CMake.AuthorError";
  141. case MessageType::FATAL_ERROR:
  142. return "CMake.FatalError";
  143. case MessageType::INTERNAL_ERROR:
  144. return "CMake.InternalError";
  145. case MessageType::DEPRECATION_ERROR:
  146. return "CMake.DeprecationError";
  147. case MessageType::MESSAGE:
  148. return "CMake.Message";
  149. case MessageType::LOG:
  150. return "CMake.Log";
  151. default:
  152. return cm::nullopt;
  153. }
  154. }
  155. Json::Value cmSarif::Rule::GetJson() const
  156. {
  157. Json::Value rule(Json::objectValue);
  158. rule["id"] = this->Id;
  159. if (this->Name) {
  160. rule["name"] = *this->Name;
  161. }
  162. if (this->FullDescription) {
  163. rule["fullDescription"]["text"] = *this->FullDescription;
  164. }
  165. if (this->DefaultMessage) {
  166. rule["messageStrings"]["default"]["text"] = *this->DefaultMessage;
  167. }
  168. return rule;
  169. }
  170. cmSarif::SourceFileLocation::SourceFileLocation(
  171. cmListFileBacktrace const& backtrace)
  172. {
  173. if (backtrace.Empty()) {
  174. throw std::runtime_error("Empty source file location");
  175. }
  176. cmListFileContext const& lfc = backtrace.Top();
  177. this->Uri = lfc.FilePath;
  178. this->Line = lfc.Line;
  179. }
  180. cm::optional<cmSarif::SourceFileLocation>
  181. cmSarif::SourceFileLocation::FromBacktrace(
  182. cmListFileBacktrace const& backtrace)
  183. {
  184. if (backtrace.Empty()) {
  185. return cm::nullopt;
  186. }
  187. cmListFileContext const& lfc = backtrace.Top();
  188. if (lfc.Line <= 0 || lfc.FilePath.empty()) {
  189. return cm::nullopt;
  190. }
  191. return cm::make_optional<cmSarif::SourceFileLocation>(backtrace);
  192. }
  193. void cmSarif::ResultsLog::WriteJson(Json::Value& root) const
  194. {
  195. // Add SARIF metadata
  196. root["version"] = "2.1.0";
  197. root["$schema"] = "https://schemastore.azurewebsites.net/schemas/json/"
  198. "sarif-2.1.0-rtm.4.json";
  199. // JSON object for the SARIF runs array
  200. Json::Value runs(Json::arrayValue);
  201. // JSON object for the current (only) run
  202. Json::Value currentRun(Json::objectValue);
  203. // Accumulate info about the reported rules
  204. Json::Value jsonRules(Json::arrayValue);
  205. for (auto const& ruleId : this->EnabledRules) {
  206. jsonRules.append(KnownRules.at(ruleId).GetJson());
  207. }
  208. // Add info the driver for the current run (CMake)
  209. Json::Value driverTool(Json::objectValue);
  210. driverTool["name"] = "CMake";
  211. driverTool["version"] = CMake_VERSION;
  212. driverTool["rules"] = jsonRules;
  213. currentRun["tool"]["driver"] = driverTool;
  214. runs.append(currentRun);
  215. // Add all results
  216. Json::Value jsonResults(Json::arrayValue);
  217. for (auto const& res : this->Results) {
  218. Json::Value jsonResult(Json::objectValue);
  219. if (res.Message) {
  220. jsonResult["message"]["text"] = *(res.Message);
  221. }
  222. // If the result has a level, add it to the result
  223. if (res.Level) {
  224. switch (*res.Level) {
  225. case ResultSeverityLevel::SARIF_WARNING:
  226. jsonResult["level"] = "warning";
  227. break;
  228. case ResultSeverityLevel::SARIF_ERROR:
  229. jsonResult["level"] = "error";
  230. break;
  231. case ResultSeverityLevel::SARIF_NOTE:
  232. jsonResult["level"] = "note";
  233. break;
  234. case ResultSeverityLevel::SARIF_NONE:
  235. jsonResult["level"] = "none";
  236. break;
  237. }
  238. }
  239. // If the result has a rule ID or index, add it to the result
  240. if (res.RuleId) {
  241. jsonResult["ruleId"] = *res.RuleId;
  242. }
  243. if (res.RuleIndex) {
  244. jsonResult["ruleIndex"] = Json::UInt64(*res.RuleIndex);
  245. }
  246. if (res.Location) {
  247. jsonResult["locations"][0]["physicalLocation"]["artifactLocation"]
  248. ["uri"] = (res.Location)->Uri;
  249. jsonResult["locations"][0]["physicalLocation"]["region"]["startLine"] =
  250. Json::Int64((res.Location)->Line);
  251. }
  252. jsonResults.append(jsonResult);
  253. }
  254. currentRun["results"] = jsonResults;
  255. runs[0] = currentRun;
  256. root["runs"] = runs;
  257. }
  258. cmSarif::LogFileWriter::~LogFileWriter()
  259. {
  260. // If the file has not been written yet, try to finalize it
  261. if (!this->FileWritten) {
  262. // Try to write and check the result
  263. if (this->TryWrite() == WriteResult::FAILURE) {
  264. // If the result is `FAILURE`, it means the write condition is true but
  265. // the file still wasn't written. This is an error.
  266. cmSystemTools::Error("Failed to write SARIF log to " +
  267. this->FilePath.generic_string());
  268. }
  269. }
  270. }
  271. bool cmSarif::LogFileWriter::EnsureFileValid()
  272. {
  273. // First, ensure directory exists
  274. cm::filesystem::path dir = this->FilePath.parent_path();
  275. if (!cmSystemTools::FileIsDirectory(dir.generic_string())) {
  276. if (!this->CreateDirectories ||
  277. !cmSystemTools::MakeDirectory(dir.generic_string()).IsSuccess()) {
  278. return false;
  279. }
  280. }
  281. // Open the file for writing
  282. cmsys::ofstream outputFile(this->FilePath.generic_string().c_str());
  283. if (!outputFile.good()) {
  284. return false;
  285. }
  286. return true;
  287. }
  288. cmSarif::LogFileWriter::WriteResult cmSarif::LogFileWriter::TryWrite()
  289. {
  290. // Check that SARIF logging is enabled
  291. if (!this->WriteCondition || !this->WriteCondition()) {
  292. return WriteResult::SKIPPED;
  293. }
  294. // Open the file
  295. if (!this->EnsureFileValid()) {
  296. return WriteResult::FAILURE;
  297. }
  298. cmsys::ofstream outputFile(this->FilePath.generic_string().c_str());
  299. // The file is available, so proceed to write the log
  300. // Assemble the SARIF JSON from the results in the log
  301. Json::Value root(Json::objectValue);
  302. this->Log.WriteJson(root);
  303. // Serialize the JSON to the file
  304. Json::StreamWriterBuilder builder;
  305. std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
  306. writer->write(root, &outputFile);
  307. outputFile.close();
  308. this->FileWritten = true;
  309. return WriteResult::SUCCESS;
  310. }
  311. bool cmSarif::LogFileWriter::ConfigureForCMakeRun(cmake& cm)
  312. {
  313. // If an explicit SARIF output path has been provided, set and check it
  314. cm::optional<std::string> sarifFilePath = cm.GetSarifFilePath();
  315. if (sarifFilePath) {
  316. this->SetPath(cm::filesystem::path(*sarifFilePath));
  317. if (!this->EnsureFileValid()) {
  318. cmSystemTools::Error(
  319. cmStrCat("Invalid SARIF output file path: ", *sarifFilePath));
  320. return false;
  321. }
  322. }
  323. // The write condition is checked immediately before writing the file, which
  324. // allows projects to enable SARIF diagnostics by setting a cache variable
  325. // and have it take effect for the current run.
  326. this->SetWriteCondition([&cm]() {
  327. // The command-line option can be used to set an explicit path, but in
  328. // normal mode, the project variable `CMAKE_EXPORT_SARIF` can also enable
  329. // SARIF logging.
  330. return cm.GetSarifFilePath().has_value() ||
  331. (cm.GetWorkingMode() == cmake::NORMAL_MODE &&
  332. cm.GetCacheDefinition(cmSarif::PROJECT_SARIF_FILE_VARIABLE).IsOn());
  333. });
  334. return true;
  335. }