forked from OSchip/llvm-project
[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:
parent
8321ad9ffc
commit
a5ddd3cacb
|
@ -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;
|
||||
|
|
|
@ -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>");
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
#define deref(X) (*X)
|
||||
|
||||
char helper(
|
||||
char *out,
|
||||
int doDereference) {
|
||||
if (doDereference) {
|
||||
return deref(out);
|
||||
} else {
|
||||
return 'x';
|
||||
}
|
||||
return 'c';
|
||||
}
|
|
@ -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}};
|
|
@ -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}};
|
|
@ -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}};
|
|
@ -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}};
|
|
@ -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}};
|
|
@ -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}};
|
Loading…
Reference in New Issue