[Analyzer] Add support for displaying cross-file diagnostic paths in HTML output

This change adds support for cross-file diagnostic paths in html output. If the
diagnostic path is not cross-file, there is no change in the output.

Patch by Vlad Tsyrklevich!

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

llvm-svn: 309968
This commit is contained in:
Devin Coughlin 2017-08-03 18:12:22 +00:00
parent fd2c379568
commit f0cadcd9f3
14 changed files with 286 additions and 130 deletions

View file

@ -28,9 +28,10 @@ ANALYSIS_CONSTRAINTS(Z3Constraints, "z3", "Use Z3 contraint solver", CreateZ3Con
#define ANALYSIS_DIAGNOSTICS(NAME, CMDFLAG, DESC, CREATEFN)
#endif
ANALYSIS_DIAGNOSTICS(HTML, "html", "Output analysis results using HTML", createHTMLDiagnosticConsumer)
ANALYSIS_DIAGNOSTICS(HTML, "html", "Output analysis results using HTML", createHTMLDiagnosticConsumer)
ANALYSIS_DIAGNOSTICS(HTML_SINGLE_FILE, "html-single-file", "Output analysis results using HTML (not allowing for multi-file bugs)", createHTMLSingleFileDiagnosticConsumer)
ANALYSIS_DIAGNOSTICS(PLIST, "plist", "Output analysis results using Plists", createPlistDiagnosticConsumer)
ANALYSIS_DIAGNOSTICS(PLIST_MULTI_FILE, "plist-multi-file", "Output analysis results using Plists (allowing for mult-file bugs)", createPlistMultiFileDiagnosticConsumer)
ANALYSIS_DIAGNOSTICS(PLIST_MULTI_FILE, "plist-multi-file", "Output analysis results using Plists (allowing for multi-file bugs)", createPlistMultiFileDiagnosticConsumer)
ANALYSIS_DIAGNOSTICS(PLIST_HTML, "plist-html", "Output analysis results using HTML wrapped with Plists", createPlistHTMLDiagnosticConsumer)
ANALYSIS_DIAGNOSTICS(TEXT, "text", "Text output of analysis results", createTextPathDiagnosticConsumer)

View file

@ -289,6 +289,11 @@ void html::AddHeaderFooterInternalBuiltinCSS(Rewriter &R, FileID FID,
" body { color:#000000; background-color:#ffffff }\n"
" body { font-family:Helvetica, sans-serif; font-size:10pt }\n"
" h1 { font-size:14pt }\n"
" .FileName { margin-top: 5px; margin-bottom: 5px; display: inline; }\n"
" .FileNav { margin-left: 5px; margin-right: 5px; display: inline; }\n"
" .FileNav a { text-decoration:none; font-size: larger; }\n"
" .divider { margin-top: 30px; margin-bottom: 30px; height: 15px; }\n"
" .divider { background-color: gray; }\n"
" .code { border-collapse:collapse; width:100%; }\n"
" .code { font-family: \"Monospace\", monospace; font-size:10pt }\n"
" .code { line-height: 1.2em }\n"

View file

@ -44,8 +44,12 @@ class HTMLDiagnostics : public PathDiagnosticConsumer {
bool createdDir, noDir;
const Preprocessor &PP;
AnalyzerOptions &AnalyzerOpts;
const bool SupportsCrossFileDiagnostics;
public:
HTMLDiagnostics(AnalyzerOptions &AnalyzerOpts, const std::string& prefix, const Preprocessor &pp);
HTMLDiagnostics(AnalyzerOptions &AnalyzerOpts,
const std::string& prefix,
const Preprocessor &pp,
bool supportsMultipleFiles);
~HTMLDiagnostics() override { FlushDiagnostics(nullptr); }
@ -56,6 +60,10 @@ public:
return "HTMLDiagnostics";
}
bool supportsCrossFileDiagnostics() const override {
return SupportsCrossFileDiagnostics;
}
unsigned ProcessMacroPiece(raw_ostream &os,
const PathDiagnosticMacroPiece& P,
unsigned num);
@ -69,21 +77,47 @@ public:
void ReportDiag(const PathDiagnostic& D,
FilesMade *filesMade);
// Generate the full HTML report
std::string GenerateHTML(const PathDiagnostic& D, Rewriter &R,
const SourceManager& SMgr, const PathPieces& path,
const char *declName);
// Add HTML header/footers to file specified by FID
void FinalizeHTML(const PathDiagnostic& D, Rewriter &R,
const SourceManager& SMgr, const PathPieces& path,
FileID FID, const FileEntry *Entry, const char *declName);
// Rewrite the file specified by FID with HTML formatting.
void RewriteFile(Rewriter &R, const SourceManager& SMgr,
const PathPieces& path, FileID FID);
};
} // end anonymous namespace
HTMLDiagnostics::HTMLDiagnostics(AnalyzerOptions &AnalyzerOpts,
const std::string& prefix,
const Preprocessor &pp)
: Directory(prefix), createdDir(false), noDir(false), PP(pp), AnalyzerOpts(AnalyzerOpts) {
}
const Preprocessor &pp,
bool supportsMultipleFiles)
: Directory(prefix),
createdDir(false),
noDir(false),
PP(pp),
AnalyzerOpts(AnalyzerOpts),
SupportsCrossFileDiagnostics(supportsMultipleFiles) {}
void ento::createHTMLDiagnosticConsumer(AnalyzerOptions &AnalyzerOpts,
PathDiagnosticConsumers &C,
const std::string& prefix,
const Preprocessor &PP) {
C.push_back(new HTMLDiagnostics(AnalyzerOpts, prefix, PP));
C.push_back(new HTMLDiagnostics(AnalyzerOpts, prefix, PP, true));
}
void ento::createHTMLSingleFileDiagnosticConsumer(AnalyzerOptions &AnalyzerOpts,
PathDiagnosticConsumers &C,
const std::string& prefix,
const Preprocessor &PP) {
C.push_back(new HTMLDiagnostics(AnalyzerOpts, prefix, PP, false));
}
//===----------------------------------------------------------------------===//
@ -121,24 +155,24 @@ void HTMLDiagnostics::ReportDiag(const PathDiagnostic& D,
// First flatten out the entire path to make it easier to use.
PathPieces path = D.path.flatten(/*ShouldFlattenMacros=*/false);
// The path as already been prechecked that all parts of the path are
// from the same file and that it is non-empty.
const SourceManager &SMgr = path.front()->getLocation().getManager();
// The path as already been prechecked that the path is non-empty.
assert(!path.empty());
FileID FID =
path.front()->getLocation().asLocation().getExpansionLoc().getFileID();
assert(FID.isValid());
const SourceManager &SMgr = path.front()->getLocation().getManager();
// Create a new rewriter to generate HTML.
Rewriter R(const_cast<SourceManager&>(SMgr), PP.getLangOpts());
// The file for the first path element is considered the main report file, it
// will usually be equivalent to SMgr.getMainFileID(); however, it might be a
// header when -analyzer-opt-analyze-headers is used.
FileID ReportFile = path.front()->getLocation().asLocation().getExpansionLoc().getFileID();
// Get the function/method name
SmallString<128> declName("unknown");
int offsetDecl = 0;
if (const Decl *DeclWithIssue = D.getDeclWithIssue()) {
if (const NamedDecl *ND = dyn_cast<NamedDecl>(DeclWithIssue)) {
if (const NamedDecl *ND = dyn_cast<NamedDecl>(DeclWithIssue))
declName = ND->getDeclName().getAsString();
}
if (const Stmt *Body = DeclWithIssue->getBody()) {
// Retrieve the relative position of the declaration which will be used
@ -151,49 +185,144 @@ void HTMLDiagnostics::ReportDiag(const PathDiagnostic& D,
}
}
// Process the path.
// Maintain the counts of extra note pieces separately.
unsigned TotalPieces = path.size();
unsigned TotalNotePieces =
std::count_if(path.begin(), path.end(),
[](const std::shared_ptr<PathDiagnosticPiece> &p) {
return isa<PathDiagnosticNotePiece>(*p);
});
std::string report = GenerateHTML(D, R, SMgr, path, declName.c_str());
if (report.empty()) {
llvm::errs() << "warning: no diagnostics generated for main file.\n";
return;
}
unsigned TotalRegularPieces = TotalPieces - TotalNotePieces;
unsigned NumRegularPieces = TotalRegularPieces;
unsigned NumNotePieces = TotalNotePieces;
// Create a path for the target HTML file.
int FD;
SmallString<128> Model, ResultPath;
for (auto I = path.rbegin(), E = path.rend(); I != E; ++I) {
if (isa<PathDiagnosticNotePiece>(I->get())) {
// This adds diagnostic bubbles, but not navigation.
// Navigation through note pieces would be added later,
// as a separate pass through the piece list.
HandlePiece(R, FID, **I, NumNotePieces, TotalNotePieces);
--NumNotePieces;
} else {
HandlePiece(R, FID, **I, NumRegularPieces, TotalRegularPieces);
--NumRegularPieces;
if (!AnalyzerOpts.shouldWriteStableReportFilename()) {
llvm::sys::path::append(Model, Directory, "report-%%%%%%.html");
if (std::error_code EC =
llvm::sys::fs::make_absolute(Model)) {
llvm::errs() << "warning: could not make '" << Model
<< "' absolute: " << EC.message() << '\n';
return;
}
if (std::error_code EC =
llvm::sys::fs::createUniqueFile(Model, FD, ResultPath)) {
llvm::errs() << "warning: could not create file in '" << Directory
<< "': " << EC.message() << '\n';
return;
}
} else {
int i = 1;
std::error_code EC;
do {
// Find a filename which is not already used
const FileEntry* Entry = SMgr.getFileEntryForID(ReportFile);
std::stringstream filename;
Model = "";
filename << "report-"
<< llvm::sys::path::filename(Entry->getName()).str()
<< "-" << declName.c_str()
<< "-" << offsetDecl
<< "-" << i << ".html";
llvm::sys::path::append(Model, Directory,
filename.str());
EC = llvm::sys::fs::openFileForWrite(Model,
FD,
llvm::sys::fs::F_RW |
llvm::sys::fs::F_Excl);
if (EC && EC != llvm::errc::file_exists) {
llvm::errs() << "warning: could not create file '" << Model
<< "': " << EC.message() << '\n';
return;
}
i++;
} while (EC);
}
llvm::raw_fd_ostream os(FD, true);
if (filesMade)
filesMade->addDiagnostic(D, getName(),
llvm::sys::path::filename(ResultPath));
// Emit the HTML to disk.
os << report;
}
std::string HTMLDiagnostics::GenerateHTML(const PathDiagnostic& D, Rewriter &R,
const SourceManager& SMgr, const PathPieces& path, const char *declName) {
// Rewrite source files as HTML for every new file the path crosses
std::vector<FileID> FileIDs;
for (auto I : path) {
FileID FID = I->getLocation().asLocation().getExpansionLoc().getFileID();
if (std::find(FileIDs.begin(), FileIDs.end(), FID) != FileIDs.end())
continue;
FileIDs.push_back(FID);
RewriteFile(R, SMgr, path, FID);
}
if (SupportsCrossFileDiagnostics && FileIDs.size() > 1) {
// Prefix file names, anchor tags, and nav cursors to every file
for (auto I = FileIDs.begin(), E = FileIDs.end(); I != E; I++) {
std::string s;
llvm::raw_string_ostream os(s);
if (I != FileIDs.begin())
os << "<hr class=divider>\n";
os << "<div id=File" << I->getHashValue() << ">\n";
// Left nav arrow
if (I != FileIDs.begin())
os << "<div class=FileNav><a href=\"#File" << (I - 1)->getHashValue()
<< "\">&#x2190;</a></div>";
os << "<h4 class=FileName>" << SMgr.getFileEntryForID(*I)->getName()
<< "</h4>\n";
// Right nav arrow
if (I + 1 != E)
os << "<div class=FileNav><a href=\"#File" << (I + 1)->getHashValue()
<< "\">&#x2192;</a></div>";
os << "</div>\n";
R.InsertTextBefore(SMgr.getLocForStartOfFile(*I), os.str());
}
// Append files to the main report file in the order they appear in the path
for (auto I : llvm::make_range(FileIDs.begin() + 1, FileIDs.end())) {
std::string s;
llvm::raw_string_ostream os(s);
const RewriteBuffer *Buf = R.getRewriteBufferFor(I);
for (auto BI : *Buf)
os << BI;
R.InsertTextAfter(SMgr.getLocForEndOfFile(FileIDs[0]), os.str());
}
}
// Add line numbers, header, footer, etc.
const RewriteBuffer *Buf = R.getRewriteBufferFor(FileIDs[0]);
if (!Buf)
return "";
// unsigned FID = R.getSourceMgr().getMainFileID();
html::EscapeText(R, FID);
html::AddLineNumbers(R, FID);
// Add CSS, header, and footer.
const FileEntry* Entry = SMgr.getFileEntryForID(FileIDs[0]);
FinalizeHTML(D, R, SMgr, path, FileIDs[0], Entry, declName);
// If we have a preprocessor, relex the file and syntax highlight.
// We might not have a preprocessor if we come from a deserialized AST file,
// for example.
std::string file;
llvm::raw_string_ostream os(file);
for (auto BI : *Buf)
os << BI;
html::SyntaxHighlight(R, FID, PP);
html::HighlightMacros(R, FID, PP);
// Get the full directory name of the analyzed file.
const FileEntry* Entry = SMgr.getFileEntryForID(FID);
return os.str();
}
void HTMLDiagnostics::FinalizeHTML(const PathDiagnostic& D, Rewriter &R,
const SourceManager& SMgr, const PathPieces& path, FileID FID,
const FileEntry *Entry, const char *declName) {
// This is a cludge; basically we want to append either the full
// working directory if we have no directory information. This is
// a work in progress.
@ -306,73 +435,48 @@ void HTMLDiagnostics::ReportDiag(const PathDiagnostic& D,
R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), os.str());
}
// Add CSS, header, and footer.
html::AddHeaderFooterInternalBuiltinCSS(R, FID, Entry->getName());
}
// Get the rewrite buffer.
const RewriteBuffer *Buf = R.getRewriteBufferFor(FID);
void HTMLDiagnostics::RewriteFile(Rewriter &R, const SourceManager& SMgr,
const PathPieces& path, FileID FID) {
// Process the path.
// Maintain the counts of extra note pieces separately.
unsigned TotalPieces = path.size();
unsigned TotalNotePieces =
std::count_if(path.begin(), path.end(),
[](const std::shared_ptr<PathDiagnosticPiece> &p) {
return isa<PathDiagnosticNotePiece>(*p);
});
if (!Buf) {
llvm::errs() << "warning: no diagnostics generated for main file.\n";
return;
unsigned TotalRegularPieces = TotalPieces - TotalNotePieces;
unsigned NumRegularPieces = TotalRegularPieces;
unsigned NumNotePieces = TotalNotePieces;
for (auto I = path.rbegin(), E = path.rend(); I != E; ++I) {
if (isa<PathDiagnosticNotePiece>(I->get())) {
// This adds diagnostic bubbles, but not navigation.
// Navigation through note pieces would be added later,
// as a separate pass through the piece list.
HandlePiece(R, FID, **I, NumNotePieces, TotalNotePieces);
--NumNotePieces;
} else {
HandlePiece(R, FID, **I, NumRegularPieces, TotalRegularPieces);
--NumRegularPieces;
}
}
// Create a path for the target HTML file.
int FD;
SmallString<128> Model, ResultPath;
// Add line numbers, header, footer, etc.
if (!AnalyzerOpts.shouldWriteStableReportFilename()) {
llvm::sys::path::append(Model, Directory, "report-%%%%%%.html");
if (std::error_code EC =
llvm::sys::fs::make_absolute(Model)) {
llvm::errs() << "warning: could not make '" << Model
<< "' absolute: " << EC.message() << '\n';
return;
}
if (std::error_code EC =
llvm::sys::fs::createUniqueFile(Model, FD, ResultPath)) {
llvm::errs() << "warning: could not create file in '" << Directory
<< "': " << EC.message() << '\n';
return;
}
html::EscapeText(R, FID);
html::AddLineNumbers(R, FID);
} else {
int i = 1;
std::error_code EC;
do {
// Find a filename which is not already used
std::stringstream filename;
Model = "";
filename << "report-"
<< llvm::sys::path::filename(Entry->getName()).str()
<< "-" << declName.c_str()
<< "-" << offsetDecl
<< "-" << i << ".html";
llvm::sys::path::append(Model, Directory,
filename.str());
EC = llvm::sys::fs::openFileForWrite(Model,
FD,
llvm::sys::fs::F_RW |
llvm::sys::fs::F_Excl);
if (EC && EC != llvm::errc::file_exists) {
llvm::errs() << "warning: could not create file '" << Model
<< "': " << EC.message() << '\n';
return;
}
i++;
} while (EC);
}
// If we have a preprocessor, relex the file and syntax highlight.
// We might not have a preprocessor if we come from a deserialized AST file,
// for example.
llvm::raw_fd_ostream os(FD, true);
if (filesMade)
filesMade->addDiagnostic(D, getName(),
llvm::sys::path::filename(ResultPath));
// Emit the HTML to disk.
for (RewriteBuffer::iterator I = Buf->begin(), E = Buf->end(); I!=E; ++I)
os << *I;
html::SyntaxHighlight(R, FID, PP);
html::HighlightMacros(R, FID, PP);
}
void HTMLDiagnostics::HandlePiece(Rewriter& R, FileID BugFileID,

View file

@ -1,12 +0,0 @@
// RUN: %clang_analyze_cc1 -analyzer-checker=core -verify %s
// RUN: %clang_analyze_cc1 -analyzer-checker=core -analyzer-output=html -o PR12421.html %s 2>&1 | FileCheck %s
// Test for PR12421
#include "diag-cross-file-boundaries.h"
int main(){
f();
return 0;
}
// CHECK: warning: Path diagnostic report is not generated.

View file

@ -0,0 +1,14 @@
// RUN: %clang_analyze_cc1 -analyzer-checker=core -verify %s
// RUN: %clang_analyze_cc1 -analyzer-checker=core -analyzer-output=html-single-file -o D30406.html %s 2>&1 | FileCheck %s
// Check that single file HTML output does not process multi-file diagnostics.
// (This used to test for PR12421, before the introduction of the html-single-file format)
#include "html-diag-singlefile.h"
int main(){
f();
return 0;
}
// CHECK: warning: Path diagnostic report is not generated.

View file

@ -0,0 +1,10 @@
// RUN: mkdir -p %t.dir
// RUN: %clang_analyze_cc1 -analyzer-opt-analyze-headers -analyzer-output=html -analyzer-checker=core -o %t.dir %s
// RUN: ls %t.dir | grep report
// RUN: rm -rf %t.dir
// This tests that we emit HTML diagnostics for reports in headers when the
// analyzer is run with -analyzer-opt-analyze-headers. This was handled
// incorrectly in the first iteration of D30406.
#include "html-diags-analyze-headers.h"

View file

@ -0,0 +1,5 @@
#include "html-diags-multifile.h"
void test_call_macro() {
has_bug(0);
}

View file

@ -1,10 +1,9 @@
// RUN: mkdir -p %t.dir
// RUN: %clang_analyze_cc1 -analyzer-output=html -analyzer-checker=core -o %t.dir %s
// RUN: ls %t.dir | not grep report
// RUN: ls %t.dir | grep report
// RUN: rm -fR %t.dir
// This tests that we do not currently emit HTML diagnostics for reports that
// cross file boundaries.
// This tests that we emit HTML diagnostics for reports that cross file boundaries.
#include "html-diags-multifile.h"

View file

@ -3,6 +3,12 @@
// RUN: %clang_analyze_cc1 -analyzer-output=html -analyzer-checker=core -o %T/dir %s
// RUN: ls %T/dir | grep report
// D30406: Test new html-single-file output
// RUN: rm -fR %T/dir
// RUN: mkdir %T/dir
// RUN: %clang_analyze_cc1 -analyzer-output=html-single-file -analyzer-checker=core -o %T/dir %s
// RUN: ls %T/dir | grep report
// PR16547: Test relative paths
// RUN: cd %T/dir
// RUN: %clang_analyze_cc1 -analyzer-output=html -analyzer-checker=core -o testrelative %s

View file

@ -1,11 +1,18 @@
// RUN: rm -rf %t
// RUN: %clang_cc1 -analyze -analyzer-output=html -analyzer-checker=core -o %t %s
// RUN: find %t -name "*.html" -exec cat "{}" ";" | FileCheck %s
//
// RUN: rm -rf %t
// RUN: %clang_cc1 -analyze -analyzer-output=html-single-file -analyzer-checker=core -o %t %s
// RUN: find %t -name "*.html" -exec cat "{}" ";" | FileCheck %s
// REQUIRES: staticanalyzer
// CHECK: <h3>Annotated Source Code</h3>
// Make sure it's not generated as a multi-file HTML output
// CHECK-NOT: <h4 class=FileName>{{.*}}
// Without tweaking expr, the expr would hit to the line below
// emitted to the output as comment.
// CHECK: {{[D]ereference of null pointer}}

View file

@ -0,0 +1,21 @@
// RUN: rm -rf %t
// RUN: %clang_cc1 -analyze -analyzer-output=html -analyzer-checker=core -o %t %s
// RUN: find %t -name "*.html" -exec cat "{}" ";" | FileCheck %s
// REQUIRES: staticanalyzer
// CHECK: <h3>Annotated Source Code</h3>
// Make sure it's generated as multi-file HTML output
// CHECK: <h4 class=FileName>{{.*}}html-multifile-diagnostics.c</h4>
// CHECK: <h4 class=FileName>{{.*}}html-multifile-diagnostics.h</h4>
// Without tweaking expr, the expr would hit to the line below
// emitted to the output as comment.
// CHECK: {{[D]ereference of null pointer}}
#include "html-multifile-diagnostics.h"
void f0() {
f1((int*)0);
}

View file

@ -0,0 +1,3 @@
void f1(int *ptr) {
*ptr = 0;
}

View file

@ -107,13 +107,6 @@ mailing list</a> to notify other members of the community.</p>
<li>Bug Reporting
<ul>
<li>Add support for displaying cross-file diagnostic paths in HTML output
(used by <tt>scan-build</tt>).
<p>Currently <tt>scan-build</tt> output does not display reports that span
multiple files. The main problem is that we do not have a good format to
display such paths in HTML output. <i>(Difficulty: Medium)</i> </p>
</li>
<li>Refactor path diagnostic generation in <a href="http://clang.llvm.org/doxygen/BugReporter_8cpp_source.html">BugReporter.cpp</a>.
<p>It would be great to have more code reuse between "Minimal" and
"Extensive" PathDiagnostic generation algorithms. One idea is to create an