xref: /llvm-project/clang/unittests/Basic/SarifTest.cpp (revision dda8ac8d3a6a7de7c1cc3f031bb5296bae74d754)
1 //===- unittests/Basic/SarifTest.cpp - Test writing SARIF documents -------===//
2 //
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6 //
7 //===----------------------------------------------------------------------===//
8 
9 #include "clang/Basic/Sarif.h"
10 #include "clang/Basic/DiagnosticOptions.h"
11 #include "clang/Basic/FileManager.h"
12 #include "clang/Basic/FileSystemOptions.h"
13 #include "clang/Basic/LangOptions.h"
14 #include "clang/Basic/SourceLocation.h"
15 #include "clang/Basic/SourceManager.h"
16 #include "llvm/ADT/StringRef.h"
17 #include "llvm/Support/FormatVariadic.h"
18 #include "llvm/Support/JSON.h"
19 #include "llvm/Support/MemoryBuffer.h"
20 #include "llvm/Support/VirtualFileSystem.h"
21 #include "llvm/Support/raw_ostream.h"
22 #include "gmock/gmock.h"
23 #include "gtest/gtest.h"
24 
25 #include <algorithm>
26 
27 using namespace clang;
28 
29 namespace {
30 
31 using LineCol = std::pair<unsigned int, unsigned int>;
32 
33 static std::string serializeSarifDocument(llvm::json::Object &&Doc) {
34   std::string Output;
35   llvm::json::Value Value(std::move(Doc));
36   llvm::raw_string_ostream OS{Output};
37   OS << llvm::formatv("{0}", Value);
38   OS.flush();
39   return Output;
40 }
41 
42 class SarifDocumentWriterTest : public ::testing::Test {
43 protected:
44   SarifDocumentWriterTest()
45       : InMemoryFileSystem(new llvm::vfs::InMemoryFileSystem),
46         FileMgr(FileSystemOptions(), InMemoryFileSystem),
47         DiagID(new DiagnosticIDs()), DiagOpts(new DiagnosticOptions()),
48         Diags(DiagID, DiagOpts.get(), new IgnoringDiagConsumer()),
49         SourceMgr(Diags, FileMgr) {}
50 
51   IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> InMemoryFileSystem;
52   FileManager FileMgr;
53   IntrusiveRefCntPtr<DiagnosticIDs> DiagID;
54   IntrusiveRefCntPtr<DiagnosticOptions> DiagOpts;
55   DiagnosticsEngine Diags;
56   SourceManager SourceMgr;
57   LangOptions LangOpts;
58 
59   FileID registerSource(llvm::StringRef Name, const char *SourceText,
60                         bool IsMainFile = false) {
61     std::unique_ptr<llvm::MemoryBuffer> SourceBuf =
62         llvm::MemoryBuffer::getMemBuffer(SourceText);
63     const FileEntry *SourceFile =
64         FileMgr.getVirtualFile(Name, SourceBuf->getBufferSize(), 0);
65     SourceMgr.overrideFileContents(SourceFile, std::move(SourceBuf));
66     FileID FID = SourceMgr.getOrCreateFileID(SourceFile, SrcMgr::C_User);
67     if (IsMainFile)
68       SourceMgr.setMainFileID(FID);
69     return FID;
70   }
71 
72   CharSourceRange getFakeCharSourceRange(FileID FID, LineCol Begin,
73                                          LineCol End) {
74     auto BeginLoc = SourceMgr.translateLineCol(FID, Begin.first, Begin.second);
75     auto EndLoc = SourceMgr.translateLineCol(FID, End.first, End.second);
76     return CharSourceRange{SourceRange{BeginLoc, EndLoc}, /* ITR = */ false};
77   }
78 };
79 
80 TEST_F(SarifDocumentWriterTest, canCreateEmptyDocument) {
81   // GIVEN:
82   SarifDocumentWriter Writer{SourceMgr};
83 
84   // WHEN:
85   const llvm::json::Object &EmptyDoc = Writer.createDocument();
86   std::vector<StringRef> Keys(EmptyDoc.size());
87   std::transform(EmptyDoc.begin(), EmptyDoc.end(), Keys.begin(),
88                  [](auto Item) { return Item.getFirst(); });
89 
90   // THEN:
91   ASSERT_THAT(Keys, testing::UnorderedElementsAre("$schema", "version"));
92 }
93 
94 // Test that a newly inserted run will associate correct tool names
95 TEST_F(SarifDocumentWriterTest, canCreateDocumentWithOneRun) {
96   // GIVEN:
97   SarifDocumentWriter Writer{SourceMgr};
98   const char *ShortName = "sariftest";
99   const char *LongName = "sarif writer test";
100 
101   // WHEN:
102   Writer.createRun(ShortName, LongName);
103   Writer.endRun();
104   const llvm::json::Object &Doc = Writer.createDocument();
105   const llvm::json::Array *Runs = Doc.getArray("runs");
106 
107   // THEN:
108   // A run was created
109   ASSERT_THAT(Runs, testing::NotNull());
110 
111   // It is the only run
112   ASSERT_EQ(Runs->size(), 1UL);
113 
114   // The tool associated with the run was the tool
115   const llvm::json::Object *Driver =
116       Runs->begin()->getAsObject()->getObject("tool")->getObject("driver");
117   ASSERT_THAT(Driver, testing::NotNull());
118 
119   ASSERT_TRUE(Driver->getString("name").has_value());
120   ASSERT_TRUE(Driver->getString("fullName").has_value());
121   ASSERT_TRUE(Driver->getString("language").has_value());
122 
123   EXPECT_EQ(*Driver->getString("name"), ShortName);
124   EXPECT_EQ(*Driver->getString("fullName"), LongName);
125   EXPECT_EQ(*Driver->getString("language"), "en-US");
126 }
127 
128 TEST_F(SarifDocumentWriterTest, addingResultsWillCrashIfThereIsNoRun) {
129 #if defined(NDEBUG) || !GTEST_HAS_DEATH_TEST
130   GTEST_SKIP() << "This death test is only available for debug builds.";
131 #endif
132   // GIVEN:
133   SarifDocumentWriter Writer{SourceMgr};
134 
135   // WHEN:
136   //  A SarifDocumentWriter::createRun(...) was not called prior to
137   //  SarifDocumentWriter::appendResult(...)
138   // But a rule exists
139   auto RuleIdx = Writer.createRule(SarifRule::create());
140   const SarifResult &EmptyResult = SarifResult::create(RuleIdx);
141 
142   // THEN:
143   auto Matcher = ::testing::AnyOf(
144       ::testing::HasSubstr("create a run first"),
145       ::testing::HasSubstr("no runs associated with the document"));
146   ASSERT_DEATH(Writer.appendResult(EmptyResult), Matcher);
147 }
148 
149 TEST_F(SarifDocumentWriterTest, settingInvalidRankWillCrash) {
150 #if defined(NDEBUG) || !GTEST_HAS_DEATH_TEST
151   GTEST_SKIP() << "This death test is only available for debug builds.";
152 #endif
153   // GIVEN:
154   SarifDocumentWriter Writer{SourceMgr};
155 
156   // WHEN:
157   // A SarifReportingConfiguration is created with an invalid "rank"
158   // * Ranks below 0.0 are invalid
159   // * Ranks above 100.0 are invalid
160 
161   // THEN: The builder will crash in either case
162   EXPECT_DEATH(SarifReportingConfiguration::create().setRank(-1.0),
163                ::testing::HasSubstr("Rule rank cannot be smaller than 0.0"));
164   EXPECT_DEATH(SarifReportingConfiguration::create().setRank(101.0),
165                ::testing::HasSubstr("Rule rank cannot be larger than 100.0"));
166 }
167 
168 TEST_F(SarifDocumentWriterTest, creatingResultWithDisabledRuleWillCrash) {
169 #if defined(NDEBUG) || !GTEST_HAS_DEATH_TEST
170   GTEST_SKIP() << "This death test is only available for debug builds.";
171 #endif
172 
173   // GIVEN:
174   SarifDocumentWriter Writer{SourceMgr};
175 
176   // WHEN:
177   // A disabled Rule is created, and a result is create referencing this rule
178   const auto &Config = SarifReportingConfiguration::create().disable();
179   auto RuleIdx =
180       Writer.createRule(SarifRule::create().setDefaultConfiguration(Config));
181   const SarifResult &Result = SarifResult::create(RuleIdx);
182 
183   // THEN:
184   // SarifResult::create(...) will produce a crash
185   ASSERT_DEATH(
186       Writer.appendResult(Result),
187       ::testing::HasSubstr("Cannot add a result referencing a disabled Rule"));
188 }
189 
190 // Test adding rule and result shows up in the final document
191 TEST_F(SarifDocumentWriterTest, addingResultWithValidRuleAndRunIsOk) {
192   // GIVEN:
193   SarifDocumentWriter Writer{SourceMgr};
194   const SarifRule &Rule =
195       SarifRule::create()
196           .setRuleId("clang.unittest")
197           .setDescription("Example rule created during unit tests")
198           .setName("clang unit test");
199 
200   // WHEN:
201   Writer.createRun("sarif test", "sarif test runner");
202   unsigned RuleIdx = Writer.createRule(Rule);
203   const SarifResult &Result = SarifResult::create(RuleIdx);
204 
205   Writer.appendResult(Result);
206   const llvm::json::Object &Doc = Writer.createDocument();
207 
208   // THEN:
209   // A document with a valid schema and version exists
210   ASSERT_THAT(Doc.get("$schema"), ::testing::NotNull());
211   ASSERT_THAT(Doc.get("version"), ::testing::NotNull());
212   const llvm::json::Array *Runs = Doc.getArray("runs");
213 
214   // A run exists on this document
215   ASSERT_THAT(Runs, ::testing::NotNull());
216   ASSERT_EQ(Runs->size(), 1UL);
217   const llvm::json::Object *TheRun = Runs->back().getAsObject();
218 
219   // The run has slots for tools, results, rules and artifacts
220   ASSERT_THAT(TheRun->get("tool"), ::testing::NotNull());
221   ASSERT_THAT(TheRun->get("results"), ::testing::NotNull());
222   ASSERT_THAT(TheRun->get("artifacts"), ::testing::NotNull());
223   const llvm::json::Object *Driver =
224       TheRun->getObject("tool")->getObject("driver");
225   const llvm::json::Array *Results = TheRun->getArray("results");
226   const llvm::json::Array *Artifacts = TheRun->getArray("artifacts");
227 
228   // The tool is as expected
229   ASSERT_TRUE(Driver->getString("name").has_value());
230   ASSERT_TRUE(Driver->getString("fullName").has_value());
231 
232   EXPECT_EQ(*Driver->getString("name"), "sarif test");
233   EXPECT_EQ(*Driver->getString("fullName"), "sarif test runner");
234 
235   // The results are as expected
236   EXPECT_EQ(Results->size(), 1UL);
237 
238   // The artifacts are as expected
239   EXPECT_TRUE(Artifacts->empty());
240 }
241 
242 TEST_F(SarifDocumentWriterTest, checkSerializingResultsWithDefaultRuleConfig) {
243   // GIVEN:
244   const std::string ExpectedOutput =
245       R"({"$schema":"https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json","runs":[{"artifacts":[],"columnKind":"unicodeCodePoints","results":[{"level":"warning","message":{"text":""},"ruleId":"clang.unittest","ruleIndex":0}],"tool":{"driver":{"fullName":"sarif test runner","informationUri":"https://clang.llvm.org/docs/UsersManual.html","language":"en-US","name":"sarif test","rules":[{"defaultConfiguration":{"enabled":true,"level":"warning","rank":-1},"fullDescription":{"text":"Example rule created during unit tests"},"id":"clang.unittest","name":"clang unit test"}],"version":"1.0.0"}}}],"version":"2.1.0"})";
246 
247   SarifDocumentWriter Writer{SourceMgr};
248   const SarifRule &Rule =
249       SarifRule::create()
250           .setRuleId("clang.unittest")
251           .setDescription("Example rule created during unit tests")
252           .setName("clang unit test");
253 
254   // WHEN: A run contains a result
255   Writer.createRun("sarif test", "sarif test runner", "1.0.0");
256   unsigned RuleIdx = Writer.createRule(Rule);
257   const SarifResult &Result = SarifResult::create(RuleIdx);
258   Writer.appendResult(Result);
259   std::string Output = serializeSarifDocument(Writer.createDocument());
260 
261   // THEN:
262   ASSERT_THAT(Output, ::testing::StrEq(ExpectedOutput));
263 }
264 
265 TEST_F(SarifDocumentWriterTest, checkSerializingResultsWithCustomRuleConfig) {
266   // GIVEN:
267   const std::string ExpectedOutput =
268       R"({"$schema":"https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json","runs":[{"artifacts":[],"columnKind":"unicodeCodePoints","results":[{"level":"error","message":{"text":""},"ruleId":"clang.unittest","ruleIndex":0}],"tool":{"driver":{"fullName":"sarif test runner","informationUri":"https://clang.llvm.org/docs/UsersManual.html","language":"en-US","name":"sarif test","rules":[{"defaultConfiguration":{"enabled":true,"level":"error","rank":35.5},"fullDescription":{"text":"Example rule created during unit tests"},"id":"clang.unittest","name":"clang unit test"}],"version":"1.0.0"}}}],"version":"2.1.0"})";
269 
270   SarifDocumentWriter Writer{SourceMgr};
271   const SarifRule &Rule =
272       SarifRule::create()
273           .setRuleId("clang.unittest")
274           .setDescription("Example rule created during unit tests")
275           .setName("clang unit test")
276           .setDefaultConfiguration(SarifReportingConfiguration::create()
277                                        .setLevel(SarifResultLevel::Error)
278                                        .setRank(35.5));
279 
280   // WHEN: A run contains a result
281   Writer.createRun("sarif test", "sarif test runner", "1.0.0");
282   unsigned RuleIdx = Writer.createRule(Rule);
283   const SarifResult &Result = SarifResult::create(RuleIdx);
284   Writer.appendResult(Result);
285   std::string Output = serializeSarifDocument(Writer.createDocument());
286 
287   // THEN:
288   ASSERT_THAT(Output, ::testing::StrEq(ExpectedOutput));
289 }
290 
291 // Check that serializing artifacts from results produces valid SARIF
292 TEST_F(SarifDocumentWriterTest, checkSerializingArtifacts) {
293   // GIVEN:
294   const std::string ExpectedOutput =
295       R"({"$schema":"https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json","runs":[{"artifacts":[{"length":40,"location":{"index":0,"uri":"file:///main.cpp"},"mimeType":"text/plain","roles":["resultFile"]}],"columnKind":"unicodeCodePoints","results":[{"level":"error","locations":[{"physicalLocation":{"artifactLocation":{"index":0,"uri":"file:///main.cpp"},"region":{"endColumn":14,"startColumn":14,"startLine":3}}}],"message":{"text":"expected ';' after top level declarator"},"ruleId":"clang.unittest","ruleIndex":0}],"tool":{"driver":{"fullName":"sarif test runner","informationUri":"https://clang.llvm.org/docs/UsersManual.html","language":"en-US","name":"sarif test","rules":[{"defaultConfiguration":{"enabled":true,"level":"warning","rank":-1},"fullDescription":{"text":"Example rule created during unit tests"},"id":"clang.unittest","name":"clang unit test"}],"version":"1.0.0"}}}],"version":"2.1.0"})";
296 
297   SarifDocumentWriter Writer{SourceMgr};
298   const SarifRule &Rule =
299       SarifRule::create()
300           .setRuleId("clang.unittest")
301           .setDescription("Example rule created during unit tests")
302           .setName("clang unit test");
303 
304   // WHEN: A result is added with valid source locations for its diagnostics
305   Writer.createRun("sarif test", "sarif test runner", "1.0.0");
306   unsigned RuleIdx = Writer.createRule(Rule);
307 
308   llvm::SmallVector<CharSourceRange, 1> DiagLocs;
309   const char *SourceText = "int foo = 0;\n"
310                            "int bar = 1;\n"
311                            "float x = 0.0\n";
312 
313   FileID MainFileID =
314       registerSource("/main.cpp", SourceText, /* IsMainFile = */ true);
315   CharSourceRange SourceCSR =
316       getFakeCharSourceRange(MainFileID, {3, 14}, {3, 14});
317 
318   DiagLocs.push_back(SourceCSR);
319 
320   const SarifResult &Result =
321       SarifResult::create(RuleIdx)
322           .setLocations(DiagLocs)
323           .setDiagnosticMessage("expected ';' after top level declarator")
324           .setDiagnosticLevel(SarifResultLevel::Error);
325   Writer.appendResult(Result);
326   std::string Output = serializeSarifDocument(Writer.createDocument());
327 
328   // THEN: Assert that the serialized SARIF is as expected
329   ASSERT_THAT(Output, ::testing::StrEq(ExpectedOutput));
330 }
331 
332 TEST_F(SarifDocumentWriterTest, checkSerializingCodeflows) {
333   // GIVEN:
334   const std::string ExpectedOutput =
335       R"({"$schema":"https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json","runs":[{"artifacts":[{"length":41,"location":{"index":0,"uri":"file:///main.cpp"},"mimeType":"text/plain","roles":["resultFile"]},{"length":27,"location":{"index":1,"uri":"file:///test-header-1.h"},"mimeType":"text/plain","roles":["resultFile"]},{"length":30,"location":{"index":2,"uri":"file:///test-header-2.h"},"mimeType":"text/plain","roles":["resultFile"]},{"length":28,"location":{"index":3,"uri":"file:///test-header-3.h"},"mimeType":"text/plain","roles":["resultFile"]}],"columnKind":"unicodeCodePoints","results":[{"codeFlows":[{"threadFlows":[{"locations":[{"importance":"essential","location":{"message":{"text":"Message #1"},"physicalLocation":{"artifactLocation":{"index":1,"uri":"file:///test-header-1.h"},"region":{"endColumn":8,"endLine":2,"startColumn":1,"startLine":1}}}},{"importance":"important","location":{"message":{"text":"Message #2"},"physicalLocation":{"artifactLocation":{"index":2,"uri":"file:///test-header-2.h"},"region":{"endColumn":8,"endLine":2,"startColumn":1,"startLine":1}}}},{"importance":"unimportant","location":{"message":{"text":"Message #3"},"physicalLocation":{"artifactLocation":{"index":3,"uri":"file:///test-header-3.h"},"region":{"endColumn":8,"endLine":2,"startColumn":1,"startLine":1}}}}]}]}],"level":"warning","locations":[{"physicalLocation":{"artifactLocation":{"index":0,"uri":"file:///main.cpp"},"region":{"endColumn":8,"endLine":2,"startColumn":5,"startLine":2}}}],"message":{"text":"Redefinition of 'foo'"},"ruleId":"clang.unittest","ruleIndex":0}],"tool":{"driver":{"fullName":"sarif test runner","informationUri":"https://clang.llvm.org/docs/UsersManual.html","language":"en-US","name":"sarif test","rules":[{"defaultConfiguration":{"enabled":true,"level":"warning","rank":-1},"fullDescription":{"text":"Example rule created during unit tests"},"id":"clang.unittest","name":"clang unit test"}],"version":"1.0.0"}}}],"version":"2.1.0"})";
336 
337   const char *SourceText = "int foo = 0;\n"
338                            "int foo = 1;\n"
339                            "float x = 0.0;\n";
340   FileID MainFileID =
341       registerSource("/main.cpp", SourceText, /* IsMainFile = */ true);
342   CharSourceRange DiagLoc{getFakeCharSourceRange(MainFileID, {2, 5}, {2, 8})};
343 
344   SarifDocumentWriter Writer{SourceMgr};
345   const SarifRule &Rule =
346       SarifRule::create()
347           .setRuleId("clang.unittest")
348           .setDescription("Example rule created during unit tests")
349           .setName("clang unit test");
350 
351   constexpr unsigned int NumCases = 3;
352   llvm::SmallVector<ThreadFlow, NumCases> Threadflows;
353   const char *HeaderTexts[NumCases]{("#pragma once\n"
354                                      "#include <foo>"),
355                                     ("#ifndef FOO\n"
356                                      "#define FOO\n"
357                                      "#endif"),
358                                     ("#ifdef FOO\n"
359                                      "#undef FOO\n"
360                                      "#endif")};
361   const char *HeaderNames[NumCases]{"/test-header-1.h", "/test-header-2.h",
362                                     "/test-header-3.h"};
363   ThreadFlowImportance Importances[NumCases]{ThreadFlowImportance::Essential,
364                                              ThreadFlowImportance::Important,
365                                              ThreadFlowImportance::Unimportant};
366   for (size_t Idx = 0; Idx != NumCases; ++Idx) {
367     FileID FID = registerSource(HeaderNames[Idx], HeaderTexts[Idx]);
368     CharSourceRange &&CSR = getFakeCharSourceRange(FID, {1, 1}, {2, 8});
369     std::string Message = llvm::formatv("Message #{0}", Idx + 1);
370     ThreadFlow Item = ThreadFlow::create()
371                           .setRange(CSR)
372                           .setImportance(Importances[Idx])
373                           .setMessage(Message);
374     Threadflows.push_back(Item);
375   }
376 
377   // WHEN: A result containing code flows and diagnostic locations is added
378   Writer.createRun("sarif test", "sarif test runner", "1.0.0");
379   unsigned RuleIdx = Writer.createRule(Rule);
380   const SarifResult &Result =
381       SarifResult::create(RuleIdx)
382           .setLocations({DiagLoc})
383           .setDiagnosticMessage("Redefinition of 'foo'")
384           .setThreadFlows(Threadflows)
385           .setDiagnosticLevel(SarifResultLevel::Warning);
386   Writer.appendResult(Result);
387   std::string Output = serializeSarifDocument(Writer.createDocument());
388 
389   // THEN: Assert that the serialized SARIF is as expected
390   ASSERT_THAT(Output, ::testing::StrEq(ExpectedOutput));
391 }
392 
393 } // namespace
394