Index: cfe/trunk/include/clang/StaticAnalyzer/Core/Analyses.def =================================================================== --- cfe/trunk/include/clang/StaticAnalyzer/Core/Analyses.def +++ cfe/trunk/include/clang/StaticAnalyzer/Core/Analyses.def @@ -28,9 +28,10 @@ #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) Index: cfe/trunk/lib/Rewrite/HTMLRewrite.cpp =================================================================== --- cfe/trunk/lib/Rewrite/HTMLRewrite.cpp +++ cfe/trunk/lib/Rewrite/HTMLRewrite.cpp @@ -289,6 +289,11 @@ " 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" Index: cfe/trunk/lib/StaticAnalyzer/Core/HTMLDiagnostics.cpp =================================================================== --- cfe/trunk/lib/StaticAnalyzer/Core/HTMLDiagnostics.cpp +++ cfe/trunk/lib/StaticAnalyzer/Core/HTMLDiagnostics.cpp @@ -44,8 +44,12 @@ 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 @@ return "HTMLDiagnostics"; } + bool supportsCrossFileDiagnostics() const override { + return SupportsCrossFileDiagnostics; + } + unsigned ProcessMacroPiece(raw_ostream &os, const PathDiagnosticMacroPiece& P, unsigned num); @@ -69,21 +77,47 @@ 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 @@ // 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(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(DeclWithIssue)) { + if (const NamedDecl *ND = dyn_cast(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 @@ } } - // 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 &p) { - return isa(*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(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); } - // Add line numbers, header, footer, etc. + llvm::raw_fd_ostream os(FD, true); - // unsigned FID = R.getSourceMgr().getMainFileID(); - html::EscapeText(R, FID); - html::AddLineNumbers(R, FID); + if (filesMade) + filesMade->addDiagnostic(D, getName(), + llvm::sys::path::filename(ResultPath)); - // 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. + // Emit the HTML to disk. + os << report; +} - html::SyntaxHighlight(R, FID, PP); - html::HighlightMacros(R, FID, PP); +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 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 << "
\n"; + + os << "
getHashValue() << ">\n"; + + // Left nav arrow + if (I != FileIDs.begin()) + os << ""; - // Get the full directory name of the analyzed file. + os << "

" << SMgr.getFileEntryForID(*I)->getName() + << "

\n"; - const FileEntry* Entry = SMgr.getFileEntryForID(FID); + // Right nav arrow + if (I + 1 != E) + os << ""; + os << "
\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()); + } + } + + const RewriteBuffer *Buf = R.getRewriteBufferFor(FileIDs[0]); + if (!Buf) + return ""; + + // Add CSS, header, and footer. + const FileEntry* Entry = SMgr.getFileEntryForID(FileIDs[0]); + FinalizeHTML(D, R, SMgr, path, FileIDs[0], Entry, declName); + + std::string file; + llvm::raw_string_ostream os(file); + for (auto BI : *Buf) + os << BI; + + 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 @@ 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); - - if (!Buf) { - llvm::errs() << "warning: no diagnostics generated for main file.\n"; - return; - } - - // Create a path for the target HTML file. - int FD; - SmallString<128> Model, ResultPath; +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 &p) { + return isa(*p); + }); - 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; - } + unsigned TotalRegularPieces = TotalPieces - TotalNotePieces; + unsigned NumRegularPieces = TotalRegularPieces; + unsigned NumNotePieces = TotalNotePieces; - } 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); + for (auto I = path.rbegin(), E = path.rend(); I != E; ++I) { + if (isa(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; + } } - llvm::raw_fd_ostream os(FD, true); + // Add line numbers, header, footer, etc. - if (filesMade) - filesMade->addDiagnostic(D, getName(), - llvm::sys::path::filename(ResultPath)); + html::EscapeText(R, FID); + html::AddLineNumbers(R, FID); - // Emit the HTML to disk. - for (RewriteBuffer::iterator I = Buf->begin(), E = Buf->end(); I!=E; ++I) - os << *I; + // 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. + + html::SyntaxHighlight(R, FID, PP); + html::HighlightMacros(R, FID, PP); } void HTMLDiagnostics::HandlePiece(Rewriter& R, FileID BugFileID, Index: cfe/trunk/test/Analysis/diagnostics/diag-cross-file-boundaries.h =================================================================== --- cfe/trunk/test/Analysis/diagnostics/diag-cross-file-boundaries.h +++ cfe/trunk/test/Analysis/diagnostics/diag-cross-file-boundaries.h @@ -1,4 +0,0 @@ -static void f() { - int *p = 0; - *p = 1; // expected-warning{{Dereference of null pointer}} -} Index: cfe/trunk/test/Analysis/diagnostics/diag-cross-file-boundaries.c =================================================================== --- cfe/trunk/test/Analysis/diagnostics/diag-cross-file-boundaries.c +++ cfe/trunk/test/Analysis/diagnostics/diag-cross-file-boundaries.c @@ -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. Index: cfe/trunk/test/Analysis/html-diag-singlefile.h =================================================================== --- cfe/trunk/test/Analysis/html-diag-singlefile.h +++ cfe/trunk/test/Analysis/html-diag-singlefile.h @@ -0,0 +1,4 @@ +static void f() { + int *p = 0; + *p = 1; // expected-warning{{Dereference of null pointer}} +} Index: cfe/trunk/test/Analysis/html-diag-singlefile.c =================================================================== --- cfe/trunk/test/Analysis/html-diag-singlefile.c +++ cfe/trunk/test/Analysis/html-diag-singlefile.c @@ -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. Index: cfe/trunk/test/Analysis/html-diags-analyze-headers.h =================================================================== --- cfe/trunk/test/Analysis/html-diags-analyze-headers.h +++ cfe/trunk/test/Analysis/html-diags-analyze-headers.h @@ -0,0 +1,5 @@ +#include "html-diags-multifile.h" + +void test_call_macro() { + has_bug(0); +} Index: cfe/trunk/test/Analysis/html-diags-analyze-headers.c =================================================================== --- cfe/trunk/test/Analysis/html-diags-analyze-headers.c +++ cfe/trunk/test/Analysis/html-diags-analyze-headers.c @@ -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" Index: cfe/trunk/test/Analysis/html-diags-multifile.c =================================================================== --- cfe/trunk/test/Analysis/html-diags-multifile.c +++ cfe/trunk/test/Analysis/html-diags-multifile.c @@ -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" Index: cfe/trunk/test/Analysis/html-diags.c =================================================================== --- cfe/trunk/test/Analysis/html-diags.c +++ cfe/trunk/test/Analysis/html-diags.c @@ -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 Index: cfe/trunk/test/Coverage/html-diagnostics.c =================================================================== --- cfe/trunk/test/Coverage/html-diagnostics.c +++ cfe/trunk/test/Coverage/html-diagnostics.c @@ -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:

Annotated Source Code

+// Make sure it's not generated as a multi-file HTML output +// CHECK-NOT:

{{.*}} + // Without tweaking expr, the expr would hit to the line below // emitted to the output as comment. // CHECK: {{[D]ereference of null pointer}} Index: cfe/trunk/test/Coverage/html-multifile-diagnostics.h =================================================================== --- cfe/trunk/test/Coverage/html-multifile-diagnostics.h +++ cfe/trunk/test/Coverage/html-multifile-diagnostics.h @@ -0,0 +1,3 @@ +void f1(int *ptr) { + *ptr = 0; +} Index: cfe/trunk/test/Coverage/html-multifile-diagnostics.c =================================================================== --- cfe/trunk/test/Coverage/html-multifile-diagnostics.c +++ cfe/trunk/test/Coverage/html-multifile-diagnostics.c @@ -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:

Annotated Source Code

+ +// Make sure it's generated as multi-file HTML output +// CHECK:

{{.*}}html-multifile-diagnostics.c

+// CHECK:

{{.*}}html-multifile-diagnostics.h

+ +// 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); +} Index: cfe/trunk/www/analyzer/open_projects.html =================================================================== --- cfe/trunk/www/analyzer/open_projects.html +++ cfe/trunk/www/analyzer/open_projects.html @@ -107,13 +107,6 @@
  • Bug Reporting
      -
    • Add support for displaying cross-file diagnostic paths in HTML output - (used by scan-build). -

      Currently scan-build 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. (Difficulty: Medium)

      -
    • -
    • Refactor path diagnostic generation in BugReporter.cpp.

      It would be great to have more code reuse between "Minimal" and "Extensive" PathDiagnostic generation algorithms. One idea is to create an