llvm-project/clang/lib/StaticAnalyzer/Core/SarifDiagnostics.cpp
Dave Bartolomeo 30402c7dea
[analyzer] Emit IssueHash in SARIF (#159445)
This change adds two new properties to each `result` object in the SARIF
log:

`partialFingerprints`: Contains the "issue hash" that the analyzer
already generates for each result, which can help identify a result
across runs even if surrounding code changes.

`hostedViewUri`: If running with `-analyzer-format=sarif-html`, this
property will now be emitted with the `file:` URL of the generated HTML
report for that result.

Along the way, I discovered an existing bug where the HTML diagnostic
consumer does not record the path to the generated report if another
compilation already created that report. This caused both the SARIF and
Plist consumers to be missing the link to the file in all but one of the
compilations in case of a warning in a header file. I added a new test
to ensure that the generated SARIF for each compilation contains the
property.

Finally, I made a few changes to the `normalize_sarif` processing in the
tests. I switched to `sed` to allow substitutions. The normalization now
removes directory components from `file:` URLs, replaces the `length`
property of the source file with a constant `-1`, and puts placeholders
in the values of the `version` properties rather than just deleting
them. The URL transformation in particular lets us verify that the right
filename is generated for each HTML report.

Fixes #158159

rdar://160410408
2025-09-25 10:47:51 -07:00

253 lines
9.5 KiB
C++

//===--- SarifDiagnostics.cpp - Sarif Diagnostics for Paths -----*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// This file defines the SarifDiagnostics object.
//
//===----------------------------------------------------------------------===//
#include "SarifDiagnostics.h"
#include "HTMLDiagnostics.h"
#include "clang/Analysis/IssueHash.h"
#include "clang/Analysis/MacroExpansionContext.h"
#include "clang/Analysis/PathDiagnostic.h"
#include "clang/Basic/Sarif.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Basic/Version.h"
#include "clang/Lex/Preprocessor.h"
#include "clang/StaticAnalyzer/Core/PathDiagnosticConsumers.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/Support/ConvertUTF.h"
#include "llvm/Support/JSON.h"
#include <memory>
using namespace llvm;
using namespace clang;
using namespace ento;
namespace {
class SarifDiagnostics : public PathDiagnosticConsumer {
std::string OutputFile;
const LangOptions &LO;
const SourceManager &SM;
SarifDocumentWriter SarifWriter;
public:
SarifDiagnostics(const std::string &Output, const LangOptions &LO,
const SourceManager &SM)
: OutputFile(Output), LO(LO), SM(SM), SarifWriter(SM) {}
~SarifDiagnostics() override = default;
void FlushDiagnosticsImpl(std::vector<const PathDiagnostic *> &Diags,
FilesMade *FM) override;
StringRef getName() const override { return "SarifDiagnostics"; }
PathGenerationScheme getGenerationScheme() const override { return Minimal; }
bool supportsLogicalOpControlFlow() const override { return true; }
bool supportsCrossFileDiagnostics() const override { return true; }
private:
SarifResult createResult(const PathDiagnostic *Diag,
const StringMap<uint32_t> &RuleMapping,
const LangOptions &LO, FilesMade *FM);
};
} // end anonymous namespace
void ento::createSarifDiagnosticConsumer(
PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C,
const std::string &Output, const Preprocessor &PP,
const cross_tu::CrossTranslationUnitContext &CTU,
const MacroExpansionContext &MacroExpansions) {
createSarifDiagnosticConsumerImpl(DiagOpts, C, Output, PP);
createTextMinimalPathDiagnosticConsumer(std::move(DiagOpts), C, Output, PP,
CTU, MacroExpansions);
}
/// Creates and registers a SARIF diagnostic consumer, without any additional
/// text consumer.
void ento::createSarifDiagnosticConsumerImpl(
PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C,
const std::string &Output, const Preprocessor &PP) {
// TODO: Emit an error here.
if (Output.empty())
return;
C.push_back(std::make_unique<SarifDiagnostics>(Output, PP.getLangOpts(),
PP.getSourceManager()));
}
static StringRef getRuleDescription(StringRef CheckName) {
return llvm::StringSwitch<StringRef>(CheckName)
#define GET_CHECKERS
#define CHECKER(FULLNAME, CLASS, HELPTEXT, DOC_URI, IS_HIDDEN) \
.Case(FULLNAME, HELPTEXT)
#include "clang/StaticAnalyzer/Checkers/Checkers.inc"
#undef CHECKER
#undef GET_CHECKERS
;
}
static StringRef getRuleHelpURIStr(StringRef CheckName) {
return llvm::StringSwitch<StringRef>(CheckName)
#define GET_CHECKERS
#define CHECKER(FULLNAME, CLASS, HELPTEXT, DOC_URI, IS_HIDDEN) \
.Case(FULLNAME, DOC_URI)
#include "clang/StaticAnalyzer/Checkers/Checkers.inc"
#undef CHECKER
#undef GET_CHECKERS
;
}
static ThreadFlowImportance
calculateImportance(const PathDiagnosticPiece &Piece) {
switch (Piece.getKind()) {
case PathDiagnosticPiece::Call:
case PathDiagnosticPiece::Macro:
case PathDiagnosticPiece::Note:
case PathDiagnosticPiece::PopUp:
// FIXME: What should be reported here?
break;
case PathDiagnosticPiece::Event:
return Piece.getTagStr() == "ConditionBRVisitor"
? ThreadFlowImportance::Important
: ThreadFlowImportance::Essential;
case PathDiagnosticPiece::ControlFlow:
return ThreadFlowImportance::Unimportant;
}
return ThreadFlowImportance::Unimportant;
}
/// Accepts a SourceRange corresponding to a pair of the first and last tokens
/// and converts to a Character granular CharSourceRange.
static CharSourceRange convertTokenRangeToCharRange(const SourceRange &R,
const SourceManager &SM,
const LangOptions &LO) {
// Caret diagnostics have the first and last locations pointed at the same
// location, return these as-is.
if (R.getBegin() == R.getEnd())
return CharSourceRange::getCharRange(R);
SourceLocation BeginCharLoc = R.getBegin();
// For token ranges, the raw end SLoc points at the first character of the
// last token in the range. This must be moved to one past the end of the
// last character using the lexer.
SourceLocation EndCharLoc =
Lexer::getLocForEndOfToken(R.getEnd(), /* Offset = */ 0, SM, LO);
return CharSourceRange::getCharRange(BeginCharLoc, EndCharLoc);
}
static SmallVector<ThreadFlow, 8> createThreadFlows(const PathDiagnostic *Diag,
const LangOptions &LO) {
SmallVector<ThreadFlow, 8> Flows;
const PathPieces &Pieces = Diag->path.flatten(false);
for (const auto &Piece : Pieces) {
auto Range = convertTokenRangeToCharRange(
Piece->getLocation().asRange(), Piece->getLocation().getManager(), LO);
auto Flow = ThreadFlow::create()
.setImportance(calculateImportance(*Piece))
.setRange(Range)
.setMessage(Piece->getString());
Flows.push_back(Flow);
}
return Flows;
}
static StringMap<uint32_t>
createRuleMapping(const std::vector<const PathDiagnostic *> &Diags,
SarifDocumentWriter &SarifWriter) {
StringMap<uint32_t> RuleMapping;
llvm::StringSet<> Seen;
for (const PathDiagnostic *D : Diags) {
StringRef CheckName = D->getCheckerName();
std::pair<llvm::StringSet<>::iterator, bool> P = Seen.insert(CheckName);
if (P.second) {
auto Rule = SarifRule::create()
.setName(CheckName)
.setRuleId(CheckName)
.setDescription(getRuleDescription(CheckName))
.setHelpURI(getRuleHelpURIStr(CheckName));
size_t RuleIdx = SarifWriter.createRule(Rule);
RuleMapping[CheckName] = RuleIdx;
}
}
return RuleMapping;
}
static const llvm::StringRef IssueHashKey = "clang/issueHash/v1";
SarifResult
SarifDiagnostics::createResult(const PathDiagnostic *Diag,
const StringMap<uint32_t> &RuleMapping,
const LangOptions &LO, FilesMade *FM) {
StringRef CheckName = Diag->getCheckerName();
uint32_t RuleIdx = RuleMapping.lookup(CheckName);
auto Range = convertTokenRangeToCharRange(
Diag->getLocation().asRange(), Diag->getLocation().getManager(), LO);
SmallVector<ThreadFlow, 8> Flows = createThreadFlows(Diag, LO);
auto IssueHash = Diag->getIssueHash(SM, LO);
std::string HtmlReportURL;
if (FM && !FM->empty()) {
// Find the HTML report that was generated for this issue, if one exists.
PDFileEntry::ConsumerFiles *Files = FM->getFiles(*Diag);
if (Files) {
auto HtmlFile =
std::find_if(Files->cbegin(), Files->cend(), [](auto &File) {
return File.first == HTML_DIAGNOSTICS_NAME;
});
if (HtmlFile != Files->cend()) {
SmallString<128> HtmlReportPath =
llvm::sys::path::parent_path(OutputFile);
llvm::sys::path::append(HtmlReportPath, HtmlFile->second);
HtmlReportURL = SarifDocumentWriter::fileNameToURI(HtmlReportPath);
}
}
}
auto Result = SarifResult::create(RuleIdx)
.setRuleId(CheckName)
.setDiagnosticMessage(Diag->getVerboseDescription())
.setDiagnosticLevel(SarifResultLevel::Warning)
.setLocations({Range})
.addPartialFingerprint(IssueHashKey, IssueHash)
.setHostedViewerURI(HtmlReportURL)
.setThreadFlows(Flows);
return Result;
}
void SarifDiagnostics::FlushDiagnosticsImpl(
std::vector<const PathDiagnostic *> &Diags, FilesMade *FM) {
// We currently overwrite the file if it already exists. However, it may be
// useful to add a feature someday that allows the user to append a run to an
// existing SARIF file. One danger from that approach is that the size of the
// file can become large very quickly, so decoding into JSON to append a run
// may be an expensive operation.
std::error_code EC;
llvm::raw_fd_ostream OS(OutputFile, EC, llvm::sys::fs::OF_TextWithCRLF);
if (EC) {
llvm::errs() << "warning: could not create file: " << EC.message() << '\n';
return;
}
std::string ToolVersion = getClangFullVersion();
SarifWriter.createRun("clang", "clang static analyzer", ToolVersion);
StringMap<uint32_t> RuleMapping = createRuleMapping(Diags, SarifWriter);
for (const PathDiagnostic *D : Diags) {
SarifResult Result = createResult(D, RuleMapping, LO, FM);
SarifWriter.appendResult(Result);
}
auto Document = SarifWriter.createDocument();
OS << llvm::formatv("{0:2}\n", json::Value(std::move(Document)));
}