[analyzer] support a mode to only show relevant lines in HTML diagnostics

HTML diagnostics can be an overwhelming blob of pages of code.
This patch adds a checkbox which filters this list down to only the
lines *relevant* to the counterexample by e.g. skipping branches which
analyzer has assumed to be infeasible at a time.

The resulting amount of output is much smaller, and often fits on one
screen, and also provides a much more readable diagnostics.

Differential Revision: https://reviews.llvm.org/D41378

llvm-svn: 322612
This commit is contained in:
George Karpenkov 2018-01-17 02:59:11 +00:00
parent 8321ad9ffc
commit a5ddd3cacb
12 changed files with 305 additions and 22 deletions

View File

@ -23,6 +23,8 @@
#include <deque>
#include <iterator>
#include <list>
#include <map>
#include <set>
#include <string>
#include <vector>
@ -733,6 +735,9 @@ public:
void Profile(llvm::FoldingSetNodeID &ID) const override;
};
/// File IDs mapped to sets of line numbers.
typedef std::map<unsigned, std::set<unsigned>> FilesToLineNumsMap;
/// PathDiagnostic - PathDiagnostic objects represent a single path-sensitive
/// diagnostic. It represents an ordered-collection of PathDiagnosticPieces,
/// each which represent the pieces of the path.
@ -756,12 +761,16 @@ class PathDiagnostic : public llvm::FoldingSetNode {
PathDiagnosticLocation UniqueingLoc;
const Decl *UniqueingDecl;
/// Lines executed in the path.
std::unique_ptr<FilesToLineNumsMap> ExecutedLines;
PathDiagnostic() = delete;
public:
PathDiagnostic(StringRef CheckName, const Decl *DeclWithIssue,
StringRef bugtype, StringRef verboseDesc, StringRef shortDesc,
StringRef category, PathDiagnosticLocation LocationToUnique,
const Decl *DeclToUnique);
const Decl *DeclToUnique,
std::unique_ptr<FilesToLineNumsMap> ExecutedLines);
~PathDiagnostic();
@ -830,6 +839,12 @@ public:
meta_iterator meta_end() const { return OtherDesc.end(); }
void addMeta(StringRef s) { OtherDesc.push_back(s); }
typedef FilesToLineNumsMap::const_iterator filesmap_iterator;
filesmap_iterator executedLines_begin() const {
return ExecutedLines->begin();
}
filesmap_iterator executedLines_end() const { return ExecutedLines->end(); }
PathDiagnosticLocation getLocation() const {
assert(Loc.isValid() && "No report location set yet!");
return Loc;

View File

@ -210,9 +210,9 @@ static void AddLineNumber(RewriteBuffer &RB, unsigned LineNo,
SmallString<256> Str;
llvm::raw_svector_ostream OS(Str);
OS << "<tr><td class=\"num\" id=\"LN"
<< LineNo << "\">"
<< LineNo << "</td><td class=\"line\">";
OS << "<tr class=\"codeline\" data-linenumber=\"" << LineNo << "\">"
<< "<td class=\"num\" id=\"LN" << LineNo << "\">" << LineNo
<< "</td><td class=\"line\">";
if (B == E) { // Handle empty lines.
OS << " </td></tr>";
@ -263,7 +263,10 @@ void html::AddLineNumbers(Rewriter& R, FileID FID) {
}
// Add one big table tag that surrounds all of the code.
RB.InsertTextBefore(0, "<table class=\"code\">\n");
std::string s;
llvm::raw_string_ostream os(s);
os << "<table class=\"code\" data-fileid=\"" << FID.getHashValue() << "\">\n";
RB.InsertTextBefore(0, os.str());
RB.InsertTextAfter(FileEnd - FileBeg, "</table>");
}

View File

@ -3509,6 +3509,66 @@ void BugReporter::FlushReport(BugReportEquivClass& EQ) {
}
}
/// Insert all lines participating in the function signature \p Signature
/// into \p ExecutedLines.
static void populateExecutedLinesWithFunctionSignature(
const Decl *Signature, SourceManager &SM,
std::unique_ptr<FilesToLineNumsMap> &ExecutedLines) {
SourceRange SignatureSourceRange;
const Stmt* Body = Signature->getBody();
if (auto FD = dyn_cast<FunctionDecl>(Signature)) {
SignatureSourceRange = FD->getSourceRange();
} else if (auto OD = dyn_cast<ObjCMethodDecl>(Signature)) {
SignatureSourceRange = OD->getSourceRange();
} else {
return;
}
SourceLocation Start = SignatureSourceRange.getBegin();
SourceLocation End = Body ? Body->getSourceRange().getBegin()
: SignatureSourceRange.getEnd();
unsigned StartLine = SM.getExpansionLineNumber(Start);
unsigned EndLine = SM.getExpansionLineNumber(End);
FileID FID = SM.getFileID(SM.getExpansionLoc(Start));
for (unsigned Line = StartLine; Line <= EndLine; Line++)
ExecutedLines->operator[](FID.getHashValue()).insert(Line);
}
/// \return all executed lines including function signatures on the path
/// starting from \p N.
static std::unique_ptr<FilesToLineNumsMap>
findExecutedLines(SourceManager &SM, const ExplodedNode *N) {
auto ExecutedLines = llvm::make_unique<FilesToLineNumsMap>();
while (N) {
if (N->getFirstPred() == nullptr) {
// First node: show signature of the entrance point.
const Decl *D = N->getLocationContext()->getDecl();
populateExecutedLinesWithFunctionSignature(D, SM, ExecutedLines);
} else if (auto CE = N->getLocationAs<CallEnter>()) {
// Inlined function: show signature.
const Decl* D = CE->getCalleeContext()->getDecl();
populateExecutedLinesWithFunctionSignature(D, SM, ExecutedLines);
} else if (const Stmt *S = PathDiagnosticLocation::getStmt(N)) {
// Otherwise: show lines associated with the processed statement.
SourceLocation Loc = S->getSourceRange().getBegin();
SourceLocation ExpansionLoc = SM.getExpansionLoc(Loc);
FileID FID = SM.getFileID(ExpansionLoc);
unsigned LineNo = SM.getExpansionLineNumber(ExpansionLoc);
ExecutedLines->operator[](FID.getHashValue()).insert(LineNo);
}
N = N->getFirstPred();
}
return ExecutedLines;
}
void BugReporter::FlushReport(BugReport *exampleReport,
PathDiagnosticConsumer &PD,
ArrayRef<BugReport*> bugReports) {
@ -3517,13 +3577,13 @@ void BugReporter::FlushReport(BugReport *exampleReport,
// Probably doesn't make a difference in practice.
BugType& BT = exampleReport->getBugType();
std::unique_ptr<PathDiagnostic> D(new PathDiagnostic(
auto D = llvm::make_unique<PathDiagnostic>(
exampleReport->getBugType().getCheckName(),
exampleReport->getDeclWithIssue(), exampleReport->getBugType().getName(),
exampleReport->getDescription(),
exampleReport->getShortDescription(/*Fallback=*/false), BT.getCategory(),
exampleReport->getUniqueingLocation(),
exampleReport->getUniqueingDecl()));
exampleReport->getUniqueingLocation(), exampleReport->getUniqueingDecl(),
findExecutedLines(getSourceManager(), exampleReport->getErrorNode()));
if (exampleReport->isPathSensitive()) {
// Generate the full path diagnostic, using the generation scheme

View File

@ -94,6 +94,13 @@ public:
/// \return Javascript for navigating the HTML report using j/k keys.
std::string generateKeyboardNavigationJavascript();
private:
/// \return JavaScript for an option to only show relevant lines.
std::string showRelevantLinesJavascript(const PathDiagnostic &D);
/// \return Executed lines from \p D in JSON format.
std::string serializeExecutedLines(const PathDiagnostic &D);
};
} // end anonymous namespace
@ -343,6 +350,10 @@ void HTMLDiagnostics::FinalizeHTML(const PathDiagnostic& D, Rewriter &R,
R.InsertTextBefore(SMgr.getLocForStartOfFile(FID),
generateKeyboardNavigationJavascript());
// Checkbox and javascript for filtering the output to the counterexample.
R.InsertTextBefore(SMgr.getLocForStartOfFile(FID),
showRelevantLinesJavascript(D));
// Add the name of the file as an <h1> tag.
{
std::string s;
@ -450,6 +461,94 @@ void HTMLDiagnostics::FinalizeHTML(const PathDiagnostic& D, Rewriter &R,
html::AddHeaderFooterInternalBuiltinCSS(R, FID, Entry->getName());
}
std::string
HTMLDiagnostics::showRelevantLinesJavascript(const PathDiagnostic &D) {
std::string s;
llvm::raw_string_ostream os(s);
os << "<script type='text/javascript'>\n";
os << serializeExecutedLines(D);
os << R"<<<(
var filterCounterexample = function (hide) {
var tables = document.getElementsByClassName("code");
for (var t=0; t<tables.length; t++) {
var table = tables[t];
var file_id = table.getAttribute("data-fileid");
var lines_in_fid = relevant_lines[file_id];
if (!lines_in_fid) {
lines_in_fid = {};
}
var lines = table.getElementsByClassName("codeline");
for (var i=0; i<lines.length; i++) {
var el = lines[i];
var lineNo = el.getAttribute("data-linenumber");
if (!lines_in_fid[lineNo]) {
if (hide) {
el.setAttribute("hidden", "");
} else {
el.removeAttribute("hidden");
}
}
}
}
}
window.addEventListener("keydown", function (event) {
if (event.defaultPrevented) {
return;
}
if (event.key == "S") {
var checked = document.getElementsByName("showCounterexample")[0].checked;
filterCounterexample(!checked);
document.getElementsByName("showCounterexample")[0].checked = !checked;
} else {
return;
}
event.preventDefault();
}, true);
document.addEventListener("DOMContentLoaded", function() {
document.querySelector('input[name="showCounterexample"]').onchange=
function (event) {
filterCounterexample(this.checked);
};
});
</script>
<form>
<input type="checkbox" name="showCounterexample" />
<label for="showCounterexample">
Show only relevant lines
</label>
</form>
)<<<";
return os.str();
}
std::string HTMLDiagnostics::serializeExecutedLines(const PathDiagnostic &D) {
std::string s;
llvm::raw_string_ostream os(s);
os << "var relevant_lines = {";
for (auto I = D.executedLines_begin(),
E = D.executedLines_end(); I != E; ++I) {
if (I != D.executedLines_begin())
os << ", ";
os << "\"" << I->first << "\": {";
for (unsigned LineNo : I->second) {
if (LineNo != *(I->second.begin()))
os << ", ";
os << "\"" << LineNo << "\": 1";
}
os << "}";
}
os << "};";
return os.str();
}
void HTMLDiagnostics::RewriteFile(Rewriter &R, const SourceManager& SMgr,
const PathPieces& path, FileID FID) {
// Process the path.

View File

@ -98,20 +98,18 @@ void PathPieces::flattenTo(PathPieces &Primary, PathPieces &Current,
PathDiagnostic::~PathDiagnostic() {}
PathDiagnostic::PathDiagnostic(StringRef CheckName, const Decl *declWithIssue,
StringRef bugtype, StringRef verboseDesc,
StringRef shortDesc, StringRef category,
PathDiagnosticLocation LocationToUnique,
const Decl *DeclToUnique)
: CheckName(CheckName),
DeclWithIssue(declWithIssue),
BugType(StripTrailingDots(bugtype)),
VerboseDesc(StripTrailingDots(verboseDesc)),
ShortDesc(StripTrailingDots(shortDesc)),
Category(StripTrailingDots(category)),
UniqueingLoc(LocationToUnique),
UniqueingDecl(DeclToUnique),
path(pathImpl) {}
PathDiagnostic::PathDiagnostic(
StringRef CheckName, const Decl *declWithIssue, StringRef bugtype,
StringRef verboseDesc, StringRef shortDesc, StringRef category,
PathDiagnosticLocation LocationToUnique, const Decl *DeclToUnique,
std::unique_ptr<FilesToLineNumsMap> ExecutedLines)
: CheckName(CheckName), DeclWithIssue(declWithIssue),
BugType(StripTrailingDots(bugtype)),
VerboseDesc(StripTrailingDots(verboseDesc)),
ShortDesc(StripTrailingDots(shortDesc)),
Category(StripTrailingDots(category)), UniqueingLoc(LocationToUnique),
UniqueingDecl(DeclToUnique), ExecutedLines(std::move(ExecutedLines)),
path(pathImpl) {}
static PathDiagnosticCallPiece *
getFirstStackedCallToHeaderFile(PathDiagnosticCallPiece *CP,

View File

@ -0,0 +1,12 @@
#define deref(X) (*X)
char helper(
char *out,
int doDereference) {
if (doDereference) {
return deref(out);
} else {
return 'x';
}
return 'c';
}

View File

@ -0,0 +1,15 @@
#define deref(X) (*X)
int f(int coin) {
if (coin) {
int *x = 0;
return deref(x);
} else {
return 0;
}
}
// RUN: rm -rf %t.output
// RUN: %clang_analyze_cc1 -analyze -analyzer-checker=core -analyzer-output html -o %t.output %s
// RUN: cat %t.output/* | FileCheck %s --match-full-lines
// CHECK: var relevant_lines = {"1": {"3": 1, "4": 1, "5": 1, "6": 1}};

View File

@ -0,0 +1,14 @@
#include "header.h"
int f(int coin) {
char *p = 0;
if (coin) {
return helper(p, coin);
}
return 0;
}
// RUN: rm -rf %t.output
// RUN: %clang_analyze_cc1 -analyze -analyzer-checker=core -analyzer-output html -o %t.output %s
// RUN: cat %t.output/* | FileCheck %s --match-full-lines
// CHECK: var relevant_lines = {"1": {"3": 1, "4": 1, "5": 1, "6": 1}, "3": {"3": 1, "4": 1, "5": 1, "6": 1, "7": 1}};

View File

@ -0,0 +1,16 @@
int f(
int coin,
int paramA,
int paramB) {
if (coin) {
int *x = 0;
return *x;
} else {
return 0;
}
}
// RUN: rm -rf %t.output
// RUN: %clang_analyze_cc1 -analyze -analyzer-checker=core -analyzer-output html -o %t.output %s
// RUN: cat %t.output/* | FileCheck %s --match-full-lines
// CHECK: var relevant_lines = {"1": {"1": 1, "2": 1, "3": 1, "4": 1, "5": 1, "6": 1, "7": 1}};

View File

@ -0,0 +1,19 @@
@interface I
- (int)func;
@end
@implementation I
- (int)func:(int *)param {
return *param;
}
@end
void foo(I *i) {
int *x = 0;
[i func:x];
}
// RUN: rm -rf %t.output
// RUN: %clang_analyze_cc1 -analyze -analyzer-checker=core -analyzer-output html -o %t.output -Wno-objc-root-class %s
// RUN: cat %t.output/* | FileCheck %s
// CHECK: var relevant_lines = {"1": {"6": 1, "7": 1, "11": 1, "12": 1, "13": 1}};

View File

@ -0,0 +1,13 @@
int f(int coin) {
if (coin) {
int *x = 0;
return *x;
} else {
return 0;
}
}
// RUN: rm -rf %t.output
// RUN: %clang_analyze_cc1 -analyze -analyzer-checker=core -analyzer-output html -o %t.output %s
// RUN: cat %t.output/* | FileCheck %s --match-full-lines
// CHECK: var relevant_lines = {"1": {"1": 1, "2": 1, "3": 1, "4": 1}};

View File

@ -0,0 +1,19 @@
#include "header.h"
int f(int coin) {
if (coin) {
int *x = 0;
return *x;
} else {
return 0;
}
}
int v(int coin) {
return coin;
}
// RUN: rm -rf %t.output
// RUN: %clang_analyze_cc1 -analyze -analyzer-checker=core -analyzer-output html -o %t.output %s
// RUN: cat %t.output/* | FileCheck %s --match-full-lines
// CHECK: var relevant_lines = {"1": {"3": 1, "4": 1, "5": 1, "6": 1}};