| 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 | |
| 32 | using namespace llvm; |
| 33 | using namespace clang; |
| 34 | using namespace ento; |
| 35 | |
| 36 | namespace { |
| 37 | class SarifDiagnostics : public PathDiagnosticConsumer { |
| 38 | std::string OutputFile; |
| 39 | const LangOptions &LO; |
| 40 | const SourceManager &SM; |
| 41 | SarifDocumentWriter SarifWriter; |
| 42 | |
| 43 | public: |
| 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 | |
| 57 | private: |
| 58 | SarifResult createResult(const PathDiagnostic *Diag, |
| 59 | const StringMap<uint32_t> &RuleMapping, |
| 60 | const LangOptions &LO, FilesMade *FM); |
| 61 | }; |
| 62 | } // end anonymous namespace |
| 63 | |
| 64 | void 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. |
| 78 | void 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 | |
| 90 | static 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 | |
| 101 | static 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 | |
| 112 | static ThreadFlowImportance |
| 113 | calculateImportance(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. |
| 135 | static 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 | |
| 152 | static 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 | |
| 166 | static StringMap<uint32_t> |
| 167 | createRuleMapping(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 | |
| 188 | static const llvm::StringRef IssueHashKey = "clang/issueHash/v1" ; |
| 189 | |
| 190 | SarifResult |
| 191 | SarifDiagnostics::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 | |
| 231 | void 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 | |