1//===--- SarifDiagnostics.cpp - Sarif Diagnostics for Paths -----*- C++ -*-===//
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// This file defines the SarifDiagnostics object.
10//
11//===----------------------------------------------------------------------===//
12
13#include "SarifDiagnostics.h"
14#include "HTMLDiagnostics.h"
15#include "clang/Analysis/IssueHash.h"
16#include "clang/Analysis/MacroExpansionContext.h"
17#include "clang/Analysis/PathDiagnostic.h"
18#include "clang/Basic/Sarif.h"
19#include "clang/Basic/SourceManager.h"
20#include "clang/Basic/Version.h"
21#include "clang/Frontend/DiagnosticRenderer.h"
22#include "clang/Lex/Lexer.h"
23#include "clang/Lex/Preprocessor.h"
24#include "clang/StaticAnalyzer/Core/PathDiagnosticConsumers.h"
25#include "llvm/ADT/StringMap.h"
26#include "llvm/Support/ConvertUTF.h"
27#include "llvm/Support/FileSystem.h"
28#include "llvm/Support/JSON.h"
29#include "llvm/Support/Path.h"
30#include <memory>
31
32using namespace llvm;
33using namespace clang;
34using namespace ento;
35
36namespace {
37class SarifDiagnostics : public PathDiagnosticConsumer {
38 std::string OutputFile;
39 const LangOptions &LO;
40 const SourceManager &SM;
41 SarifDocumentWriter SarifWriter;
42
43public:
44 SarifDiagnostics(const std::string &Output, const LangOptions &LO,
45 const SourceManager &SM)
46 : OutputFile(Output), LO(LO), SM(SM), SarifWriter(SM) {}
47 ~SarifDiagnostics() override = default;
48
49 void FlushDiagnosticsImpl(std::vector<const PathDiagnostic *> &Diags,
50 FilesMade *FM) override;
51
52 StringRef getName() const override { return "SarifDiagnostics"; }
53 PathGenerationScheme getGenerationScheme() const override { return Minimal; }
54 bool supportsLogicalOpControlFlow() const override { return true; }
55 bool supportsCrossFileDiagnostics() const override { return true; }
56
57private:
58 SarifResult createResult(const PathDiagnostic *Diag,
59 const StringMap<uint32_t> &RuleMapping,
60 const LangOptions &LO, FilesMade *FM);
61};
62} // end anonymous namespace
63
64void ento::createSarifDiagnosticConsumer(
65 PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C,
66 const std::string &Output, const Preprocessor &PP,
67 const cross_tu::CrossTranslationUnitContext &CTU,
68 const MacroExpansionContext &MacroExpansions) {
69
70 createSarifDiagnosticConsumerImpl(DiagOpts, C, Output, PP);
71
72 createTextMinimalPathDiagnosticConsumer(Diagopts: std::move(DiagOpts), C, Prefix: Output, PP,
73 CTU, MacroExpansions);
74}
75
76/// Creates and registers a SARIF diagnostic consumer, without any additional
77/// text consumer.
78void ento::createSarifDiagnosticConsumerImpl(
79 PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C,
80 const std::string &Output, const Preprocessor &PP) {
81
82 // TODO: Emit an error here.
83 if (Output.empty())
84 return;
85
86 C.push_back(x: std::make_unique<SarifDiagnostics>(args: Output, args: PP.getLangOpts(),
87 args&: PP.getSourceManager()));
88}
89
90static StringRef getRuleDescription(StringRef CheckName) {
91 return llvm::StringSwitch<StringRef>(CheckName)
92#define GET_CHECKERS
93#define CHECKER(FULLNAME, CLASS, HELPTEXT, DOC_URI, IS_HIDDEN) \
94 .Case(FULLNAME, HELPTEXT)
95#include "clang/StaticAnalyzer/Checkers/Checkers.inc"
96#undef CHECKER
97#undef GET_CHECKERS
98 ;
99}
100
101static StringRef getRuleHelpURIStr(StringRef CheckName) {
102 return llvm::StringSwitch<StringRef>(CheckName)
103#define GET_CHECKERS
104#define CHECKER(FULLNAME, CLASS, HELPTEXT, DOC_URI, IS_HIDDEN) \
105 .Case(FULLNAME, DOC_URI)
106#include "clang/StaticAnalyzer/Checkers/Checkers.inc"
107#undef CHECKER
108#undef GET_CHECKERS
109 ;
110}
111
112static ThreadFlowImportance
113calculateImportance(const PathDiagnosticPiece &Piece) {
114 switch (Piece.getKind()) {
115 case PathDiagnosticPiece::Call:
116 case PathDiagnosticPiece::Macro:
117 case PathDiagnosticPiece::Note:
118 case PathDiagnosticPiece::PopUp:
119 // FIXME: What should be reported here?
120 break;
121 case PathDiagnosticPiece::Event:
122 return Piece.getTagStr() == "ConditionBRVisitor"
123 ? ThreadFlowImportance::Important
124 : ThreadFlowImportance::Essential;
125 case PathDiagnosticPiece::ControlFlow:
126 return ThreadFlowImportance::Unimportant;
127 }
128 return ThreadFlowImportance::Unimportant;
129}
130
131/// Returns the character range to report for \p Loc.
132///
133/// A thread flow needs a location for every piece, so an unusable range falls
134/// back to a caret rather than being dropped, which would truncate the path.
135static CharSourceRange getDisplayCharRange(const PathDiagnosticLocation &Loc,
136 const LangOptions &LO) {
137 const SourceManager &SM = Loc.getManager();
138 FullSourceLoc Caret = Loc.asLocation().getExpansionLoc();
139 SourceRange Range = Loc.asRange();
140
141 // FIXME: A single-token range is reported as a zero-width region. Widening it
142 // would churn every expected-sarif file, so it is left alone for now.
143 if (Range.getBegin() != Range.getEnd()) {
144 if (std::optional<CharSourceRange> FileRange = getExpansionRangeInFile(
145 Range: CharSourceRange::getTokenRange(R: Range), FID: Caret.getFileID(), SM))
146 return Lexer::getAsCharRange(Range: *FileRange, SM, LangOpts: LO);
147 }
148
149 return CharSourceRange::getCharRange(B: Caret, E: Caret);
150}
151
152static SmallVector<ThreadFlow, 8> createThreadFlows(const PathDiagnostic *Diag,
153 const LangOptions &LO) {
154 SmallVector<ThreadFlow, 8> Flows;
155 const PathPieces &Pieces = Diag->path.flatten(ShouldFlattenMacros: false);
156 for (const auto &Piece : Pieces) {
157 auto Flow = ThreadFlow::create()
158 .setImportance(calculateImportance(Piece: *Piece))
159 .setRange(getDisplayCharRange(Loc: Piece->getLocation(), LO))
160 .setMessage(Piece->getString());
161 Flows.push_back(Elt: Flow);
162 }
163 return Flows;
164}
165
166static StringMap<uint32_t>
167createRuleMapping(const std::vector<const PathDiagnostic *> &Diags,
168 SarifDocumentWriter &SarifWriter) {
169 StringMap<uint32_t> RuleMapping;
170 llvm::StringSet<> Seen;
171
172 for (const PathDiagnostic *D : Diags) {
173 StringRef CheckName = D->getCheckerName();
174 std::pair<llvm::StringSet<>::iterator, bool> P = Seen.insert(key: CheckName);
175 if (P.second) {
176 auto Rule = SarifRule::create()
177 .setName(CheckName)
178 .setRuleId(CheckName)
179 .setDescription(getRuleDescription(CheckName))
180 .setHelpURI(getRuleHelpURIStr(CheckName));
181 size_t RuleIdx = SarifWriter.createRule(Rule);
182 RuleMapping[CheckName] = RuleIdx;
183 }
184 }
185 return RuleMapping;
186}
187
188static const llvm::StringRef IssueHashKey = "clang/issueHash/v1";
189
190SarifResult
191SarifDiagnostics::createResult(const PathDiagnostic *Diag,
192 const StringMap<uint32_t> &RuleMapping,
193 const LangOptions &LO, FilesMade *FM) {
194
195 StringRef CheckName = Diag->getCheckerName();
196 uint32_t RuleIdx = RuleMapping.lookup(Key: CheckName);
197 CharSourceRange Range = getDisplayCharRange(Loc: Diag->getLocation(), LO);
198
199 SmallVector<ThreadFlow, 8> Flows = createThreadFlows(Diag, LO);
200
201 auto IssueHash = Diag->getIssueHash(SrcMgr: SM, LangOpts: LO);
202
203 std::string HtmlReportURL;
204 if (FM && !FM->empty()) {
205 // Find the HTML report that was generated for this issue, if one exists.
206 PDFileEntry::ConsumerFiles *Files = FM->getFiles(PD: *Diag);
207 if (Files) {
208 auto HtmlFile = llvm::find_if(Range&: *Files, P: [](const auto &File) {
209 return File.first == HTML_DIAGNOSTICS_NAME;
210 });
211 if (HtmlFile != Files->end()) {
212 SmallString<128> HtmlReportPath =
213 llvm::sys::path::parent_path(path: OutputFile);
214 llvm::sys::path::append(path&: HtmlReportPath, a: HtmlFile->second);
215 HtmlReportURL = SarifDocumentWriter::fileNameToURI(Filename: HtmlReportPath);
216 }
217 }
218 }
219
220 auto Result = SarifResult::create(RuleIdx)
221 .setRuleId(CheckName)
222 .setDiagnosticMessage(Diag->getVerboseDescription())
223 .setDiagnosticLevel(SarifResultLevel::Warning)
224 .addLocations(DiagLocs: {Range})
225 .addPartialFingerprint(key: IssueHashKey, value: IssueHash)
226 .setHostedViewerURI(HtmlReportURL)
227 .setThreadFlows(Flows);
228 return Result;
229}
230
231void SarifDiagnostics::FlushDiagnosticsImpl(
232 std::vector<const PathDiagnostic *> &Diags, FilesMade *FM) {
233 // We currently overwrite the file if it already exists. However, it may be
234 // useful to add a feature someday that allows the user to append a run to an
235 // existing SARIF file. One danger from that approach is that the size of the
236 // file can become large very quickly, so decoding into JSON to append a run
237 // may be an expensive operation.
238 std::error_code EC;
239 llvm::raw_fd_ostream OS(OutputFile, EC, llvm::sys::fs::OF_TextWithCRLF);
240 if (EC) {
241 llvm::errs() << "warning: could not create file: " << EC.message() << '\n';
242 return;
243 }
244
245 std::string ToolVersion = getClangFullVersion();
246 SarifWriter.createRun(ShortToolName: "clang", LongToolName: "clang static analyzer", ToolVersion);
247 StringMap<uint32_t> RuleMapping = createRuleMapping(Diags, SarifWriter);
248 for (const PathDiagnostic *D : Diags) {
249 SarifResult Result = createResult(Diag: D, RuleMapping, LO, FM);
250 SarifWriter.appendResult(SarifResult: Result);
251 }
252 auto Document = SarifWriter.createDocument();
253 OS << llvm::formatv(Fmt: "{0:2}\n", Vals: json::Value(std::move(Document)));
254}
255