1 //===- HTMLDiagnostics.cpp - HTML Diagnostics for Paths -------------------===//
2 //
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6 //
7 //===----------------------------------------------------------------------===//
8 //
9 //  This file defines the HTMLDiagnostics object.
10 //
11 //===----------------------------------------------------------------------===//
12 
13 #include "clang/AST/Decl.h"
14 #include "clang/AST/DeclBase.h"
15 #include "clang/AST/Stmt.h"
16 #include "clang/Analysis/IssueHash.h"
17 #include "clang/Analysis/MacroExpansionContext.h"
18 #include "clang/Analysis/PathDiagnostic.h"
19 #include "clang/Basic/FileManager.h"
20 #include "clang/Basic/LLVM.h"
21 #include "clang/Basic/SourceLocation.h"
22 #include "clang/Basic/SourceManager.h"
23 #include "clang/Lex/Lexer.h"
24 #include "clang/Lex/Preprocessor.h"
25 #include "clang/Lex/Token.h"
26 #include "clang/Rewrite/Core/HTMLRewrite.h"
27 #include "clang/Rewrite/Core/Rewriter.h"
28 #include "clang/StaticAnalyzer/Core/PathDiagnosticConsumers.h"
29 #include "llvm/ADT/ArrayRef.h"
30 #include "llvm/ADT/SmallString.h"
31 #include "llvm/ADT/StringRef.h"
32 #include "llvm/ADT/iterator_range.h"
33 #include "llvm/Support/Casting.h"
34 #include "llvm/Support/Errc.h"
35 #include "llvm/Support/ErrorHandling.h"
36 #include "llvm/Support/FileSystem.h"
37 #include "llvm/Support/MemoryBuffer.h"
38 #include "llvm/Support/Path.h"
39 #include "llvm/Support/raw_ostream.h"
40 #include <algorithm>
41 #include <cassert>
42 #include <map>
43 #include <memory>
44 #include <set>
45 #include <sstream>
46 #include <string>
47 #include <system_error>
48 #include <utility>
49 #include <vector>
50 
51 using namespace clang;
52 using namespace ento;
53 
54 //===----------------------------------------------------------------------===//
55 // Boilerplate.
56 //===----------------------------------------------------------------------===//
57 
58 namespace {
59 
60 class HTMLDiagnostics : public PathDiagnosticConsumer {
61   PathDiagnosticConsumerOptions DiagOpts;
62   std::string Directory;
63   bool createdDir = false;
64   bool noDir = false;
65   const Preprocessor &PP;
66   const bool SupportsCrossFileDiagnostics;
67 
68 public:
HTMLDiagnostics(PathDiagnosticConsumerOptions DiagOpts,const std::string & OutputDir,const Preprocessor & pp,bool supportsMultipleFiles)69   HTMLDiagnostics(PathDiagnosticConsumerOptions DiagOpts,
70                   const std::string &OutputDir, const Preprocessor &pp,
71                   bool supportsMultipleFiles)
72       : DiagOpts(std::move(DiagOpts)), Directory(OutputDir), PP(pp),
73         SupportsCrossFileDiagnostics(supportsMultipleFiles) {}
74 
~HTMLDiagnostics()75   ~HTMLDiagnostics() override { FlushDiagnostics(nullptr); }
76 
77   void FlushDiagnosticsImpl(std::vector<const PathDiagnostic *> &Diags,
78                             FilesMade *filesMade) override;
79 
getName() const80   StringRef getName() const override {
81     return "HTMLDiagnostics";
82   }
83 
supportsCrossFileDiagnostics() const84   bool supportsCrossFileDiagnostics() const override {
85     return SupportsCrossFileDiagnostics;
86   }
87 
88   unsigned ProcessMacroPiece(raw_ostream &os,
89                              const PathDiagnosticMacroPiece& P,
90                              unsigned num);
91 
92   void HandlePiece(Rewriter &R, FileID BugFileID, const PathDiagnosticPiece &P,
93                    const std::vector<SourceRange> &PopUpRanges, unsigned num,
94                    unsigned max);
95 
96   void HighlightRange(Rewriter& R, FileID BugFileID, SourceRange Range,
97                       const char *HighlightStart = "<span class=\"mrange\">",
98                       const char *HighlightEnd = "</span>");
99 
100   void ReportDiag(const PathDiagnostic& D,
101                   FilesMade *filesMade);
102 
103   // Generate the full HTML report
104   std::string GenerateHTML(const PathDiagnostic& D, Rewriter &R,
105                            const SourceManager& SMgr, const PathPieces& path,
106                            const char *declName);
107 
108   // Add HTML header/footers to file specified by FID
109   void FinalizeHTML(const PathDiagnostic& D, Rewriter &R,
110                     const SourceManager& SMgr, const PathPieces& path,
111                     FileID FID, const FileEntry *Entry, const char *declName);
112 
113   // Rewrite the file specified by FID with HTML formatting.
114   void RewriteFile(Rewriter &R, const PathPieces& path, FileID FID);
115 
116 
117 private:
118   /// \return Javascript for displaying shortcuts help;
119   StringRef showHelpJavascript();
120 
121   /// \return Javascript for navigating the HTML report using j/k keys.
122   StringRef generateKeyboardNavigationJavascript();
123 
124   /// \return JavaScript for an option to only show relevant lines.
125   std::string showRelevantLinesJavascript(
126     const PathDiagnostic &D, const PathPieces &path);
127 
128   /// Write executed lines from \p D in JSON format into \p os.
129   void dumpCoverageData(const PathDiagnostic &D,
130                         const PathPieces &path,
131                         llvm::raw_string_ostream &os);
132 };
133 
134 } // namespace
135 
createHTMLDiagnosticConsumer(PathDiagnosticConsumerOptions DiagOpts,PathDiagnosticConsumers & C,const std::string & OutputDir,const Preprocessor & PP,const cross_tu::CrossTranslationUnitContext & CTU,const MacroExpansionContext & MacroExpansions)136 void ento::createHTMLDiagnosticConsumer(
137     PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C,
138     const std::string &OutputDir, const Preprocessor &PP,
139     const cross_tu::CrossTranslationUnitContext &CTU,
140     const MacroExpansionContext &MacroExpansions) {
141 
142   // FIXME: HTML is currently our default output type, but if the output
143   // directory isn't specified, it acts like if it was in the minimal text
144   // output mode. This doesn't make much sense, we should have the minimal text
145   // as our default. In the case of backward compatibility concerns, this could
146   // be preserved with -analyzer-config-compatibility-mode=true.
147   createTextMinimalPathDiagnosticConsumer(DiagOpts, C, OutputDir, PP, CTU,
148                                           MacroExpansions);
149 
150   // TODO: Emit an error here.
151   if (OutputDir.empty())
152     return;
153 
154   C.push_back(new HTMLDiagnostics(std::move(DiagOpts), OutputDir, PP, true));
155 }
156 
createHTMLSingleFileDiagnosticConsumer(PathDiagnosticConsumerOptions DiagOpts,PathDiagnosticConsumers & C,const std::string & OutputDir,const Preprocessor & PP,const cross_tu::CrossTranslationUnitContext & CTU,const clang::MacroExpansionContext & MacroExpansions)157 void ento::createHTMLSingleFileDiagnosticConsumer(
158     PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C,
159     const std::string &OutputDir, const Preprocessor &PP,
160     const cross_tu::CrossTranslationUnitContext &CTU,
161     const clang::MacroExpansionContext &MacroExpansions) {
162   createTextMinimalPathDiagnosticConsumer(DiagOpts, C, OutputDir, PP, CTU,
163                                           MacroExpansions);
164 
165   // TODO: Emit an error here.
166   if (OutputDir.empty())
167     return;
168 
169   C.push_back(new HTMLDiagnostics(std::move(DiagOpts), OutputDir, PP, false));
170 }
171 
createPlistHTMLDiagnosticConsumer(PathDiagnosticConsumerOptions DiagOpts,PathDiagnosticConsumers & C,const std::string & prefix,const Preprocessor & PP,const cross_tu::CrossTranslationUnitContext & CTU,const MacroExpansionContext & MacroExpansions)172 void ento::createPlistHTMLDiagnosticConsumer(
173     PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C,
174     const std::string &prefix, const Preprocessor &PP,
175     const cross_tu::CrossTranslationUnitContext &CTU,
176     const MacroExpansionContext &MacroExpansions) {
177   createHTMLDiagnosticConsumer(
178       DiagOpts, C, std::string(llvm::sys::path::parent_path(prefix)), PP, CTU,
179       MacroExpansions);
180   createPlistMultiFileDiagnosticConsumer(DiagOpts, C, prefix, PP, CTU,
181                                          MacroExpansions);
182   createTextMinimalPathDiagnosticConsumer(std::move(DiagOpts), C, prefix, PP,
183                                           CTU, MacroExpansions);
184 }
185 
createSarifHTMLDiagnosticConsumer(PathDiagnosticConsumerOptions DiagOpts,PathDiagnosticConsumers & C,const std::string & sarif_file,const Preprocessor & PP,const cross_tu::CrossTranslationUnitContext & CTU,const MacroExpansionContext & MacroExpansions)186 void ento::createSarifHTMLDiagnosticConsumer(
187     PathDiagnosticConsumerOptions DiagOpts, PathDiagnosticConsumers &C,
188     const std::string &sarif_file, const Preprocessor &PP,
189     const cross_tu::CrossTranslationUnitContext &CTU,
190     const MacroExpansionContext &MacroExpansions) {
191   createHTMLDiagnosticConsumer(
192       DiagOpts, C, std::string(llvm::sys::path::parent_path(sarif_file)), PP,
193       CTU, MacroExpansions);
194   createSarifDiagnosticConsumer(DiagOpts, C, sarif_file, PP, CTU,
195                                 MacroExpansions);
196   createTextMinimalPathDiagnosticConsumer(std::move(DiagOpts), C, sarif_file,
197                                           PP, CTU, MacroExpansions);
198 }
199 
200 //===----------------------------------------------------------------------===//
201 // Report processing.
202 //===----------------------------------------------------------------------===//
203 
FlushDiagnosticsImpl(std::vector<const PathDiagnostic * > & Diags,FilesMade * filesMade)204 void HTMLDiagnostics::FlushDiagnosticsImpl(
205   std::vector<const PathDiagnostic *> &Diags,
206   FilesMade *filesMade) {
207   for (const auto Diag : Diags)
208     ReportDiag(*Diag, filesMade);
209 }
210 
ReportDiag(const PathDiagnostic & D,FilesMade * filesMade)211 void HTMLDiagnostics::ReportDiag(const PathDiagnostic& D,
212                                  FilesMade *filesMade) {
213   // Create the HTML directory if it is missing.
214   if (!createdDir) {
215     createdDir = true;
216     if (std::error_code ec = llvm::sys::fs::create_directories(Directory)) {
217       llvm::errs() << "warning: could not create directory '"
218                    << Directory << "': " << ec.message() << '\n';
219       noDir = true;
220       return;
221     }
222   }
223 
224   if (noDir)
225     return;
226 
227   // First flatten out the entire path to make it easier to use.
228   PathPieces path = D.path.flatten(/*ShouldFlattenMacros=*/false);
229 
230   // The path as already been prechecked that the path is non-empty.
231   assert(!path.empty());
232   const SourceManager &SMgr = path.front()->getLocation().getManager();
233 
234   // Create a new rewriter to generate HTML.
235   Rewriter R(const_cast<SourceManager&>(SMgr), PP.getLangOpts());
236 
237   // The file for the first path element is considered the main report file, it
238   // will usually be equivalent to SMgr.getMainFileID(); however, it might be a
239   // header when -analyzer-opt-analyze-headers is used.
240   FileID ReportFile = path.front()->getLocation().asLocation().getExpansionLoc().getFileID();
241 
242   // Get the function/method name
243   SmallString<128> declName("unknown");
244   int offsetDecl = 0;
245   if (const Decl *DeclWithIssue = D.getDeclWithIssue()) {
246       if (const auto *ND = dyn_cast<NamedDecl>(DeclWithIssue))
247           declName = ND->getDeclName().getAsString();
248 
249       if (const Stmt *Body = DeclWithIssue->getBody()) {
250           // Retrieve the relative position of the declaration which will be used
251           // for the file name
252           FullSourceLoc L(
253               SMgr.getExpansionLoc(path.back()->getLocation().asLocation()),
254               SMgr);
255           FullSourceLoc FunL(SMgr.getExpansionLoc(Body->getBeginLoc()), SMgr);
256           offsetDecl = L.getExpansionLineNumber() - FunL.getExpansionLineNumber();
257       }
258   }
259 
260   std::string report = GenerateHTML(D, R, SMgr, path, declName.c_str());
261   if (report.empty()) {
262     llvm::errs() << "warning: no diagnostics generated for main file.\n";
263     return;
264   }
265 
266   // Create a path for the target HTML file.
267   int FD;
268   SmallString<128> Model, ResultPath;
269 
270   if (!DiagOpts.ShouldWriteStableReportFilename) {
271       llvm::sys::path::append(Model, Directory, "report-%%%%%%.html");
272       if (std::error_code EC =
273           llvm::sys::fs::make_absolute(Model)) {
274           llvm::errs() << "warning: could not make '" << Model
275                        << "' absolute: " << EC.message() << '\n';
276         return;
277       }
278       if (std::error_code EC = llvm::sys::fs::createUniqueFile(
279               Model, FD, ResultPath, llvm::sys::fs::OF_Text)) {
280         llvm::errs() << "warning: could not create file in '" << Directory
281                      << "': " << EC.message() << '\n';
282         return;
283       }
284   } else {
285       int i = 1;
286       std::error_code EC;
287       do {
288           // Find a filename which is not already used
289           const FileEntry* Entry = SMgr.getFileEntryForID(ReportFile);
290           std::stringstream filename;
291           Model = "";
292           filename << "report-"
293                    << llvm::sys::path::filename(Entry->getName()).str()
294                    << "-" << declName.c_str()
295                    << "-" << offsetDecl
296                    << "-" << i << ".html";
297           llvm::sys::path::append(Model, Directory,
298                                   filename.str());
299           EC = llvm::sys::fs::openFileForReadWrite(
300               Model, FD, llvm::sys::fs::CD_CreateNew, llvm::sys::fs::OF_None);
301           if (EC && EC != llvm::errc::file_exists) {
302               llvm::errs() << "warning: could not create file '" << Model
303                            << "': " << EC.message() << '\n';
304               return;
305           }
306           i++;
307       } while (EC);
308   }
309 
310   llvm::raw_fd_ostream os(FD, true);
311 
312   if (filesMade)
313     filesMade->addDiagnostic(D, getName(),
314                              llvm::sys::path::filename(ResultPath));
315 
316   // Emit the HTML to disk.
317   os << report;
318 }
319 
GenerateHTML(const PathDiagnostic & D,Rewriter & R,const SourceManager & SMgr,const PathPieces & path,const char * declName)320 std::string HTMLDiagnostics::GenerateHTML(const PathDiagnostic& D, Rewriter &R,
321     const SourceManager& SMgr, const PathPieces& path, const char *declName) {
322   // Rewrite source files as HTML for every new file the path crosses
323   std::vector<FileID> FileIDs;
324   for (auto I : path) {
325     FileID FID = I->getLocation().asLocation().getExpansionLoc().getFileID();
326     if (llvm::is_contained(FileIDs, FID))
327       continue;
328 
329     FileIDs.push_back(FID);
330     RewriteFile(R, path, FID);
331   }
332 
333   if (SupportsCrossFileDiagnostics && FileIDs.size() > 1) {
334     // Prefix file names, anchor tags, and nav cursors to every file
335     for (auto I = FileIDs.begin(), E = FileIDs.end(); I != E; I++) {
336       std::string s;
337       llvm::raw_string_ostream os(s);
338 
339       if (I != FileIDs.begin())
340         os << "<hr class=divider>\n";
341 
342       os << "<div id=File" << I->getHashValue() << ">\n";
343 
344       // Left nav arrow
345       if (I != FileIDs.begin())
346         os << "<div class=FileNav><a href=\"#File" << (I - 1)->getHashValue()
347            << "\">&#x2190;</a></div>";
348 
349       os << "<h4 class=FileName>" << SMgr.getFileEntryForID(*I)->getName()
350          << "</h4>\n";
351 
352       // Right nav arrow
353       if (I + 1 != E)
354         os << "<div class=FileNav><a href=\"#File" << (I + 1)->getHashValue()
355            << "\">&#x2192;</a></div>";
356 
357       os << "</div>\n";
358 
359       R.InsertTextBefore(SMgr.getLocForStartOfFile(*I), os.str());
360     }
361 
362     // Append files to the main report file in the order they appear in the path
363     for (auto I : llvm::make_range(FileIDs.begin() + 1, FileIDs.end())) {
364       std::string s;
365       llvm::raw_string_ostream os(s);
366 
367       const RewriteBuffer *Buf = R.getRewriteBufferFor(I);
368       for (auto BI : *Buf)
369         os << BI;
370 
371       R.InsertTextAfter(SMgr.getLocForEndOfFile(FileIDs[0]), os.str());
372     }
373   }
374 
375   const RewriteBuffer *Buf = R.getRewriteBufferFor(FileIDs[0]);
376   if (!Buf)
377     return {};
378 
379   // Add CSS, header, and footer.
380   FileID FID =
381       path.back()->getLocation().asLocation().getExpansionLoc().getFileID();
382   const FileEntry* Entry = SMgr.getFileEntryForID(FID);
383   FinalizeHTML(D, R, SMgr, path, FileIDs[0], Entry, declName);
384 
385   std::string file;
386   llvm::raw_string_ostream os(file);
387   for (auto BI : *Buf)
388     os << BI;
389 
390   return os.str();
391 }
392 
dumpCoverageData(const PathDiagnostic & D,const PathPieces & path,llvm::raw_string_ostream & os)393 void HTMLDiagnostics::dumpCoverageData(
394     const PathDiagnostic &D,
395     const PathPieces &path,
396     llvm::raw_string_ostream &os) {
397 
398   const FilesToLineNumsMap &ExecutedLines = D.getExecutedLines();
399 
400   os << "var relevant_lines = {";
401   for (auto I = ExecutedLines.begin(),
402             E = ExecutedLines.end(); I != E; ++I) {
403     if (I != ExecutedLines.begin())
404       os << ", ";
405 
406     os << "\"" << I->first.getHashValue() << "\": {";
407     for (unsigned LineNo : I->second) {
408       if (LineNo != *(I->second.begin()))
409         os << ", ";
410 
411       os << "\"" << LineNo << "\": 1";
412     }
413     os << "}";
414   }
415 
416   os << "};";
417 }
418 
showRelevantLinesJavascript(const PathDiagnostic & D,const PathPieces & path)419 std::string HTMLDiagnostics::showRelevantLinesJavascript(
420       const PathDiagnostic &D, const PathPieces &path) {
421   std::string s;
422   llvm::raw_string_ostream os(s);
423   os << "<script type='text/javascript'>\n";
424   dumpCoverageData(D, path, os);
425   os << R"<<<(
426 
427 var filterCounterexample = function (hide) {
428   var tables = document.getElementsByClassName("code");
429   for (var t=0; t<tables.length; t++) {
430     var table = tables[t];
431     var file_id = table.getAttribute("data-fileid");
432     var lines_in_fid = relevant_lines[file_id];
433     if (!lines_in_fid) {
434       lines_in_fid = {};
435     }
436     var lines = table.getElementsByClassName("codeline");
437     for (var i=0; i<lines.length; i++) {
438         var el = lines[i];
439         var lineNo = el.getAttribute("data-linenumber");
440         if (!lines_in_fid[lineNo]) {
441           if (hide) {
442             el.setAttribute("hidden", "");
443           } else {
444             el.removeAttribute("hidden");
445           }
446         }
447     }
448   }
449 }
450 
451 window.addEventListener("keydown", function (event) {
452   if (event.defaultPrevented) {
453     return;
454   }
455   if (event.key == "S") {
456     var checked = document.getElementsByName("showCounterexample")[0].checked;
457     filterCounterexample(!checked);
458     document.getElementsByName("showCounterexample")[0].checked = !checked;
459   } else {
460     return;
461   }
462   event.preventDefault();
463 }, true);
464 
465 document.addEventListener("DOMContentLoaded", function() {
466     document.querySelector('input[name="showCounterexample"]').onchange=
467         function (event) {
468       filterCounterexample(this.checked);
469     };
470 });
471 </script>
472 
473 <form>
474     <input type="checkbox" name="showCounterexample" id="showCounterexample" />
475     <label for="showCounterexample">
476        Show only relevant lines
477     </label>
478 </form>
479 )<<<";
480 
481   return os.str();
482 }
483 
FinalizeHTML(const PathDiagnostic & D,Rewriter & R,const SourceManager & SMgr,const PathPieces & path,FileID FID,const FileEntry * Entry,const char * declName)484 void HTMLDiagnostics::FinalizeHTML(const PathDiagnostic& D, Rewriter &R,
485     const SourceManager& SMgr, const PathPieces& path, FileID FID,
486     const FileEntry *Entry, const char *declName) {
487   // This is a cludge; basically we want to append either the full
488   // working directory if we have no directory information.  This is
489   // a work in progress.
490 
491   llvm::SmallString<0> DirName;
492 
493   if (llvm::sys::path::is_relative(Entry->getName())) {
494     llvm::sys::fs::current_path(DirName);
495     DirName += '/';
496   }
497 
498   int LineNumber = path.back()->getLocation().asLocation().getExpansionLineNumber();
499   int ColumnNumber = path.back()->getLocation().asLocation().getExpansionColumnNumber();
500 
501   R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), showHelpJavascript());
502 
503   R.InsertTextBefore(SMgr.getLocForStartOfFile(FID),
504                      generateKeyboardNavigationJavascript());
505 
506   // Checkbox and javascript for filtering the output to the counterexample.
507   R.InsertTextBefore(SMgr.getLocForStartOfFile(FID),
508                      showRelevantLinesJavascript(D, path));
509 
510   // Add the name of the file as an <h1> tag.
511   {
512     std::string s;
513     llvm::raw_string_ostream os(s);
514 
515     os << "<!-- REPORTHEADER -->\n"
516        << "<h3>Bug Summary</h3>\n<table class=\"simpletable\">\n"
517           "<tr><td class=\"rowname\">File:</td><td>"
518        << html::EscapeText(DirName)
519        << html::EscapeText(Entry->getName())
520        << "</td></tr>\n<tr><td class=\"rowname\">Warning:</td><td>"
521           "<a href=\"#EndPath\">line "
522        << LineNumber
523        << ", column "
524        << ColumnNumber
525        << "</a><br />"
526        << D.getVerboseDescription() << "</td></tr>\n";
527 
528     // The navigation across the extra notes pieces.
529     unsigned NumExtraPieces = 0;
530     for (const auto &Piece : path) {
531       if (const auto *P = dyn_cast<PathDiagnosticNotePiece>(Piece.get())) {
532         int LineNumber =
533             P->getLocation().asLocation().getExpansionLineNumber();
534         int ColumnNumber =
535             P->getLocation().asLocation().getExpansionColumnNumber();
536         os << "<tr><td class=\"rowname\">Note:</td><td>"
537            << "<a href=\"#Note" << NumExtraPieces << "\">line "
538            << LineNumber << ", column " << ColumnNumber << "</a><br />"
539            << P->getString() << "</td></tr>";
540         ++NumExtraPieces;
541       }
542     }
543 
544     // Output any other meta data.
545 
546     for (PathDiagnostic::meta_iterator I = D.meta_begin(), E = D.meta_end();
547          I != E; ++I) {
548       os << "<tr><td></td><td>" << html::EscapeText(*I) << "</td></tr>\n";
549     }
550 
551     os << R"<<<(
552 </table>
553 <!-- REPORTSUMMARYEXTRA -->
554 <h3>Annotated Source Code</h3>
555 <p>Press <a href="#" onclick="toggleHelp(); return false;">'?'</a>
556    to see keyboard shortcuts</p>
557 <input type="checkbox" class="spoilerhider" id="showinvocation" />
558 <label for="showinvocation" >Show analyzer invocation</label>
559 <div class="spoiler">clang -cc1 )<<<";
560     os << html::EscapeText(DiagOpts.ToolInvocation);
561     os << R"<<<(
562 </div>
563 <div id='tooltiphint' hidden="true">
564   <p>Keyboard shortcuts: </p>
565   <ul>
566     <li>Use 'j/k' keys for keyboard navigation</li>
567     <li>Use 'Shift+S' to show/hide relevant lines</li>
568     <li>Use '?' to toggle this window</li>
569   </ul>
570   <a href="#" onclick="toggleHelp(); return false;">Close</a>
571 </div>
572 )<<<";
573     R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), os.str());
574   }
575 
576   // Embed meta-data tags.
577   {
578     std::string s;
579     llvm::raw_string_ostream os(s);
580 
581     StringRef BugDesc = D.getVerboseDescription();
582     if (!BugDesc.empty())
583       os << "\n<!-- BUGDESC " << BugDesc << " -->\n";
584 
585     StringRef BugType = D.getBugType();
586     if (!BugType.empty())
587       os << "\n<!-- BUGTYPE " << BugType << " -->\n";
588 
589     PathDiagnosticLocation UPDLoc = D.getUniqueingLoc();
590     FullSourceLoc L(SMgr.getExpansionLoc(UPDLoc.isValid()
591                                              ? UPDLoc.asLocation()
592                                              : D.getLocation().asLocation()),
593                     SMgr);
594     const Decl *DeclWithIssue = D.getDeclWithIssue();
595 
596     StringRef BugCategory = D.getCategory();
597     if (!BugCategory.empty())
598       os << "\n<!-- BUGCATEGORY " << BugCategory << " -->\n";
599 
600     os << "\n<!-- BUGFILE " << DirName << Entry->getName() << " -->\n";
601 
602     os << "\n<!-- FILENAME " << llvm::sys::path::filename(Entry->getName()) << " -->\n";
603 
604     os  << "\n<!-- FUNCTIONNAME " <<  declName << " -->\n";
605 
606     os << "\n<!-- ISSUEHASHCONTENTOFLINEINCONTEXT "
607        << getIssueHash(L, D.getCheckerName(), D.getBugType(), DeclWithIssue,
608                        PP.getLangOpts())
609        << " -->\n";
610 
611     os << "\n<!-- BUGLINE "
612        << LineNumber
613        << " -->\n";
614 
615     os << "\n<!-- BUGCOLUMN "
616       << ColumnNumber
617       << " -->\n";
618 
619     os << "\n<!-- BUGPATHLENGTH " << path.size() << " -->\n";
620 
621     // Mark the end of the tags.
622     os << "\n<!-- BUGMETAEND -->\n";
623 
624     // Insert the text.
625     R.InsertTextBefore(SMgr.getLocForStartOfFile(FID), os.str());
626   }
627 
628   html::AddHeaderFooterInternalBuiltinCSS(R, FID, Entry->getName());
629 }
630 
showHelpJavascript()631 StringRef HTMLDiagnostics::showHelpJavascript() {
632   return R"<<<(
633 <script type='text/javascript'>
634 
635 var toggleHelp = function() {
636     var hint = document.querySelector("#tooltiphint");
637     var attributeName = "hidden";
638     if (hint.hasAttribute(attributeName)) {
639       hint.removeAttribute(attributeName);
640     } else {
641       hint.setAttribute("hidden", "true");
642     }
643 };
644 window.addEventListener("keydown", function (event) {
645   if (event.defaultPrevented) {
646     return;
647   }
648   if (event.key == "?") {
649     toggleHelp();
650   } else {
651     return;
652   }
653   event.preventDefault();
654 });
655 </script>
656 )<<<";
657 }
658 
shouldDisplayPopUpRange(const SourceRange & Range)659 static bool shouldDisplayPopUpRange(const SourceRange &Range) {
660   return !(Range.getBegin().isMacroID() || Range.getEnd().isMacroID());
661 }
662 
663 static void
HandlePopUpPieceStartTag(Rewriter & R,const std::vector<SourceRange> & PopUpRanges)664 HandlePopUpPieceStartTag(Rewriter &R,
665                          const std::vector<SourceRange> &PopUpRanges) {
666   for (const auto &Range : PopUpRanges) {
667     if (!shouldDisplayPopUpRange(Range))
668       continue;
669 
670     html::HighlightRange(R, Range.getBegin(), Range.getEnd(), "",
671                          "<table class='variable_popup'><tbody>",
672                          /*IsTokenRange=*/true);
673   }
674 }
675 
HandlePopUpPieceEndTag(Rewriter & R,const PathDiagnosticPopUpPiece & Piece,std::vector<SourceRange> & PopUpRanges,unsigned int LastReportedPieceIndex,unsigned int PopUpPieceIndex)676 static void HandlePopUpPieceEndTag(Rewriter &R,
677                                    const PathDiagnosticPopUpPiece &Piece,
678                                    std::vector<SourceRange> &PopUpRanges,
679                                    unsigned int LastReportedPieceIndex,
680                                    unsigned int PopUpPieceIndex) {
681   SmallString<256> Buf;
682   llvm::raw_svector_ostream Out(Buf);
683 
684   SourceRange Range(Piece.getLocation().asRange());
685   if (!shouldDisplayPopUpRange(Range))
686     return;
687 
688   // Write out the path indices with a right arrow and the message as a row.
689   Out << "<tr><td valign='top'><div class='PathIndex PathIndexPopUp'>"
690       << LastReportedPieceIndex;
691 
692   // Also annotate the state transition with extra indices.
693   Out << '.' << PopUpPieceIndex;
694 
695   Out << "</div></td><td>" << Piece.getString() << "</td></tr>";
696 
697   // If no report made at this range mark the variable and add the end tags.
698   if (std::find(PopUpRanges.begin(), PopUpRanges.end(), Range) ==
699       PopUpRanges.end()) {
700     // Store that we create a report at this range.
701     PopUpRanges.push_back(Range);
702 
703     Out << "</tbody></table></span>";
704     html::HighlightRange(R, Range.getBegin(), Range.getEnd(),
705                          "<span class='variable'>", Buf.c_str(),
706                          /*IsTokenRange=*/true);
707   } else {
708     // Otherwise inject just the new row at the end of the range.
709     html::HighlightRange(R, Range.getBegin(), Range.getEnd(), "", Buf.c_str(),
710                          /*IsTokenRange=*/true);
711   }
712 }
713 
RewriteFile(Rewriter & R,const PathPieces & path,FileID FID)714 void HTMLDiagnostics::RewriteFile(Rewriter &R,
715                                   const PathPieces& path, FileID FID) {
716   // Process the path.
717   // Maintain the counts of extra note pieces separately.
718   unsigned TotalPieces = path.size();
719   unsigned TotalNotePieces = std::count_if(
720       path.begin(), path.end(), [](const PathDiagnosticPieceRef &p) {
721         return isa<PathDiagnosticNotePiece>(*p);
722       });
723   unsigned PopUpPieceCount = std::count_if(
724       path.begin(), path.end(), [](const PathDiagnosticPieceRef &p) {
725         return isa<PathDiagnosticPopUpPiece>(*p);
726       });
727 
728   unsigned TotalRegularPieces = TotalPieces - TotalNotePieces - PopUpPieceCount;
729   unsigned NumRegularPieces = TotalRegularPieces;
730   unsigned NumNotePieces = TotalNotePieces;
731   // Stores the count of the regular piece indices.
732   std::map<int, int> IndexMap;
733 
734   // Stores the different ranges where we have reported something.
735   std::vector<SourceRange> PopUpRanges;
736   for (auto I = path.rbegin(), E = path.rend(); I != E; ++I) {
737     const auto &Piece = *I->get();
738 
739     if (isa<PathDiagnosticPopUpPiece>(Piece)) {
740       ++IndexMap[NumRegularPieces];
741     } else if (isa<PathDiagnosticNotePiece>(Piece)) {
742       // This adds diagnostic bubbles, but not navigation.
743       // Navigation through note pieces would be added later,
744       // as a separate pass through the piece list.
745       HandlePiece(R, FID, Piece, PopUpRanges, NumNotePieces, TotalNotePieces);
746       --NumNotePieces;
747     } else {
748       HandlePiece(R, FID, Piece, PopUpRanges, NumRegularPieces,
749                   TotalRegularPieces);
750       --NumRegularPieces;
751     }
752   }
753 
754   // Secondary indexing if we are having multiple pop-ups between two notes.
755   // (e.g. [(13) 'a' is 'true'];  [(13.1) 'b' is 'false'];  [(13.2) 'c' is...)
756   NumRegularPieces = TotalRegularPieces;
757   for (auto I = path.rbegin(), E = path.rend(); I != E; ++I) {
758     const auto &Piece = *I->get();
759 
760     if (const auto *PopUpP = dyn_cast<PathDiagnosticPopUpPiece>(&Piece)) {
761       int PopUpPieceIndex = IndexMap[NumRegularPieces];
762 
763       // Pop-up pieces needs the index of the last reported piece and its count
764       // how many times we report to handle multiple reports on the same range.
765       // This marks the variable, adds the </table> end tag and the message
766       // (list element) as a row. The <table> start tag will be added after the
767       // rows has been written out. Note: It stores every different range.
768       HandlePopUpPieceEndTag(R, *PopUpP, PopUpRanges, NumRegularPieces,
769                              PopUpPieceIndex);
770 
771       if (PopUpPieceIndex > 0)
772         --IndexMap[NumRegularPieces];
773 
774     } else if (!isa<PathDiagnosticNotePiece>(Piece)) {
775       --NumRegularPieces;
776     }
777   }
778 
779   // Add the <table> start tag of pop-up pieces based on the stored ranges.
780   HandlePopUpPieceStartTag(R, PopUpRanges);
781 
782   // Add line numbers, header, footer, etc.
783   html::EscapeText(R, FID);
784   html::AddLineNumbers(R, FID);
785 
786   // If we have a preprocessor, relex the file and syntax highlight.
787   // We might not have a preprocessor if we come from a deserialized AST file,
788   // for example.
789   html::SyntaxHighlight(R, FID, PP);
790   html::HighlightMacros(R, FID, PP);
791 }
792 
HandlePiece(Rewriter & R,FileID BugFileID,const PathDiagnosticPiece & P,const std::vector<SourceRange> & PopUpRanges,unsigned num,unsigned max)793 void HTMLDiagnostics::HandlePiece(Rewriter &R, FileID BugFileID,
794                                   const PathDiagnosticPiece &P,
795                                   const std::vector<SourceRange> &PopUpRanges,
796                                   unsigned num, unsigned max) {
797   // For now, just draw a box above the line in question, and emit the
798   // warning.
799   FullSourceLoc Pos = P.getLocation().asLocation();
800 
801   if (!Pos.isValid())
802     return;
803 
804   SourceManager &SM = R.getSourceMgr();
805   assert(&Pos.getManager() == &SM && "SourceManagers are different!");
806   std::pair<FileID, unsigned> LPosInfo = SM.getDecomposedExpansionLoc(Pos);
807 
808   if (LPosInfo.first != BugFileID)
809     return;
810 
811   llvm::MemoryBufferRef Buf = SM.getBufferOrFake(LPosInfo.first);
812   const char *FileStart = Buf.getBufferStart();
813 
814   // Compute the column number.  Rewind from the current position to the start
815   // of the line.
816   unsigned ColNo = SM.getColumnNumber(LPosInfo.first, LPosInfo.second);
817   const char *TokInstantiationPtr =Pos.getExpansionLoc().getCharacterData();
818   const char *LineStart = TokInstantiationPtr-ColNo;
819 
820   // Compute LineEnd.
821   const char *LineEnd = TokInstantiationPtr;
822   const char *FileEnd = Buf.getBufferEnd();
823   while (*LineEnd != '\n' && LineEnd != FileEnd)
824     ++LineEnd;
825 
826   // Compute the margin offset by counting tabs and non-tabs.
827   unsigned PosNo = 0;
828   for (const char* c = LineStart; c != TokInstantiationPtr; ++c)
829     PosNo += *c == '\t' ? 8 : 1;
830 
831   // Create the html for the message.
832 
833   const char *Kind = nullptr;
834   bool IsNote = false;
835   bool SuppressIndex = (max == 1);
836   switch (P.getKind()) {
837   case PathDiagnosticPiece::Event: Kind = "Event"; break;
838   case PathDiagnosticPiece::ControlFlow: Kind = "Control"; break;
839     // Setting Kind to "Control" is intentional.
840   case PathDiagnosticPiece::Macro: Kind = "Control"; break;
841   case PathDiagnosticPiece::Note:
842     Kind = "Note";
843     IsNote = true;
844     SuppressIndex = true;
845     break;
846   case PathDiagnosticPiece::Call:
847   case PathDiagnosticPiece::PopUp:
848     llvm_unreachable("Calls and extra notes should already be handled");
849   }
850 
851   std::string sbuf;
852   llvm::raw_string_ostream os(sbuf);
853 
854   os << "\n<tr><td class=\"num\"></td><td class=\"line\"><div id=\"";
855 
856   if (IsNote)
857     os << "Note" << num;
858   else if (num == max)
859     os << "EndPath";
860   else
861     os << "Path" << num;
862 
863   os << "\" class=\"msg";
864   if (Kind)
865     os << " msg" << Kind;
866   os << "\" style=\"margin-left:" << PosNo << "ex";
867 
868   // Output a maximum size.
869   if (!isa<PathDiagnosticMacroPiece>(P)) {
870     // Get the string and determining its maximum substring.
871     const auto &Msg = P.getString();
872     unsigned max_token = 0;
873     unsigned cnt = 0;
874     unsigned len = Msg.size();
875 
876     for (char C : Msg)
877       switch (C) {
878       default:
879         ++cnt;
880         continue;
881       case ' ':
882       case '\t':
883       case '\n':
884         if (cnt > max_token) max_token = cnt;
885         cnt = 0;
886       }
887 
888     if (cnt > max_token)
889       max_token = cnt;
890 
891     // Determine the approximate size of the message bubble in em.
892     unsigned em;
893     const unsigned max_line = 120;
894 
895     if (max_token >= max_line)
896       em = max_token / 2;
897     else {
898       unsigned characters = max_line;
899       unsigned lines = len / max_line;
900 
901       if (lines > 0) {
902         for (; characters > max_token; --characters)
903           if (len / characters > lines) {
904             ++characters;
905             break;
906           }
907       }
908 
909       em = characters / 2;
910     }
911 
912     if (em < max_line/2)
913       os << "; max-width:" << em << "em";
914   }
915   else
916     os << "; max-width:100em";
917 
918   os << "\">";
919 
920   if (!SuppressIndex) {
921     os << "<table class=\"msgT\"><tr><td valign=\"top\">";
922     os << "<div class=\"PathIndex";
923     if (Kind) os << " PathIndex" << Kind;
924     os << "\">" << num << "</div>";
925 
926     if (num > 1) {
927       os << "</td><td><div class=\"PathNav\"><a href=\"#Path"
928          << (num - 1)
929          << "\" title=\"Previous event ("
930          << (num - 1)
931          << ")\">&#x2190;</a></div>";
932     }
933 
934     os << "</td><td>";
935   }
936 
937   if (const auto *MP = dyn_cast<PathDiagnosticMacroPiece>(&P)) {
938     os << "Within the expansion of the macro '";
939 
940     // Get the name of the macro by relexing it.
941     {
942       FullSourceLoc L = MP->getLocation().asLocation().getExpansionLoc();
943       assert(L.isFileID());
944       StringRef BufferInfo = L.getBufferData();
945       std::pair<FileID, unsigned> LocInfo = L.getDecomposedLoc();
946       const char* MacroName = LocInfo.second + BufferInfo.data();
947       Lexer rawLexer(SM.getLocForStartOfFile(LocInfo.first), PP.getLangOpts(),
948                      BufferInfo.begin(), MacroName, BufferInfo.end());
949 
950       Token TheTok;
951       rawLexer.LexFromRawLexer(TheTok);
952       for (unsigned i = 0, n = TheTok.getLength(); i < n; ++i)
953         os << MacroName[i];
954     }
955 
956     os << "':\n";
957 
958     if (!SuppressIndex) {
959       os << "</td>";
960       if (num < max) {
961         os << "<td><div class=\"PathNav\"><a href=\"#";
962         if (num == max - 1)
963           os << "EndPath";
964         else
965           os << "Path" << (num + 1);
966         os << "\" title=\"Next event ("
967         << (num + 1)
968         << ")\">&#x2192;</a></div></td>";
969       }
970 
971       os << "</tr></table>";
972     }
973 
974     // Within a macro piece.  Write out each event.
975     ProcessMacroPiece(os, *MP, 0);
976   }
977   else {
978     os << html::EscapeText(P.getString());
979 
980     if (!SuppressIndex) {
981       os << "</td>";
982       if (num < max) {
983         os << "<td><div class=\"PathNav\"><a href=\"#";
984         if (num == max - 1)
985           os << "EndPath";
986         else
987           os << "Path" << (num + 1);
988         os << "\" title=\"Next event ("
989            << (num + 1)
990            << ")\">&#x2192;</a></div></td>";
991       }
992 
993       os << "</tr></table>";
994     }
995   }
996 
997   os << "</div></td></tr>";
998 
999   // Insert the new html.
1000   unsigned DisplayPos = LineEnd - FileStart;
1001   SourceLocation Loc =
1002     SM.getLocForStartOfFile(LPosInfo.first).getLocWithOffset(DisplayPos);
1003 
1004   R.InsertTextBefore(Loc, os.str());
1005 
1006   // Now highlight the ranges.
1007   ArrayRef<SourceRange> Ranges = P.getRanges();
1008   for (const auto &Range : Ranges) {
1009     // If we have already highlighted the range as a pop-up there is no work.
1010     if (std::find(PopUpRanges.begin(), PopUpRanges.end(), Range) !=
1011         PopUpRanges.end())
1012       continue;
1013 
1014     HighlightRange(R, LPosInfo.first, Range);
1015   }
1016 }
1017 
EmitAlphaCounter(raw_ostream & os,unsigned n)1018 static void EmitAlphaCounter(raw_ostream &os, unsigned n) {
1019   unsigned x = n % ('z' - 'a');
1020   n /= 'z' - 'a';
1021 
1022   if (n > 0)
1023     EmitAlphaCounter(os, n);
1024 
1025   os << char('a' + x);
1026 }
1027 
ProcessMacroPiece(raw_ostream & os,const PathDiagnosticMacroPiece & P,unsigned num)1028 unsigned HTMLDiagnostics::ProcessMacroPiece(raw_ostream &os,
1029                                             const PathDiagnosticMacroPiece& P,
1030                                             unsigned num) {
1031   for (const auto &subPiece : P.subPieces) {
1032     if (const auto *MP = dyn_cast<PathDiagnosticMacroPiece>(subPiece.get())) {
1033       num = ProcessMacroPiece(os, *MP, num);
1034       continue;
1035     }
1036 
1037     if (const auto *EP = dyn_cast<PathDiagnosticEventPiece>(subPiece.get())) {
1038       os << "<div class=\"msg msgEvent\" style=\"width:94%; "
1039             "margin-left:5px\">"
1040             "<table class=\"msgT\"><tr>"
1041             "<td valign=\"top\"><div class=\"PathIndex PathIndexEvent\">";
1042       EmitAlphaCounter(os, num++);
1043       os << "</div></td><td valign=\"top\">"
1044          << html::EscapeText(EP->getString())
1045          << "</td></tr></table></div>\n";
1046     }
1047   }
1048 
1049   return num;
1050 }
1051 
HighlightRange(Rewriter & R,FileID BugFileID,SourceRange Range,const char * HighlightStart,const char * HighlightEnd)1052 void HTMLDiagnostics::HighlightRange(Rewriter& R, FileID BugFileID,
1053                                      SourceRange Range,
1054                                      const char *HighlightStart,
1055                                      const char *HighlightEnd) {
1056   SourceManager &SM = R.getSourceMgr();
1057   const LangOptions &LangOpts = R.getLangOpts();
1058 
1059   SourceLocation InstantiationStart = SM.getExpansionLoc(Range.getBegin());
1060   unsigned StartLineNo = SM.getExpansionLineNumber(InstantiationStart);
1061 
1062   SourceLocation InstantiationEnd = SM.getExpansionLoc(Range.getEnd());
1063   unsigned EndLineNo = SM.getExpansionLineNumber(InstantiationEnd);
1064 
1065   if (EndLineNo < StartLineNo)
1066     return;
1067 
1068   if (SM.getFileID(InstantiationStart) != BugFileID ||
1069       SM.getFileID(InstantiationEnd) != BugFileID)
1070     return;
1071 
1072   // Compute the column number of the end.
1073   unsigned EndColNo = SM.getExpansionColumnNumber(InstantiationEnd);
1074   unsigned OldEndColNo = EndColNo;
1075 
1076   if (EndColNo) {
1077     // Add in the length of the token, so that we cover multi-char tokens.
1078     EndColNo += Lexer::MeasureTokenLength(Range.getEnd(), SM, LangOpts)-1;
1079   }
1080 
1081   // Highlight the range.  Make the span tag the outermost tag for the
1082   // selected range.
1083 
1084   SourceLocation E =
1085     InstantiationEnd.getLocWithOffset(EndColNo - OldEndColNo);
1086 
1087   html::HighlightRange(R, InstantiationStart, E, HighlightStart, HighlightEnd);
1088 }
1089 
generateKeyboardNavigationJavascript()1090 StringRef HTMLDiagnostics::generateKeyboardNavigationJavascript() {
1091   return R"<<<(
1092 <script type='text/javascript'>
1093 var digitMatcher = new RegExp("[0-9]+");
1094 
1095 var querySelectorAllArray = function(selector) {
1096   return Array.prototype.slice.call(
1097     document.querySelectorAll(selector));
1098 }
1099 
1100 document.addEventListener("DOMContentLoaded", function() {
1101     querySelectorAllArray(".PathNav > a").forEach(
1102         function(currentValue, currentIndex) {
1103             var hrefValue = currentValue.getAttribute("href");
1104             currentValue.onclick = function() {
1105                 scrollTo(document.querySelector(hrefValue));
1106                 return false;
1107             };
1108         });
1109 });
1110 
1111 var findNum = function() {
1112     var s = document.querySelector(".selected");
1113     if (!s || s.id == "EndPath") {
1114         return 0;
1115     }
1116     var out = parseInt(digitMatcher.exec(s.id)[0]);
1117     return out;
1118 };
1119 
1120 var scrollTo = function(el) {
1121     querySelectorAllArray(".selected").forEach(function(s) {
1122         s.classList.remove("selected");
1123     });
1124     el.classList.add("selected");
1125     window.scrollBy(0, el.getBoundingClientRect().top -
1126         (window.innerHeight / 2));
1127 }
1128 
1129 var move = function(num, up, numItems) {
1130   if (num == 1 && up || num == numItems - 1 && !up) {
1131     return 0;
1132   } else if (num == 0 && up) {
1133     return numItems - 1;
1134   } else if (num == 0 && !up) {
1135     return 1 % numItems;
1136   }
1137   return up ? num - 1 : num + 1;
1138 }
1139 
1140 var numToId = function(num) {
1141   if (num == 0) {
1142     return document.getElementById("EndPath")
1143   }
1144   return document.getElementById("Path" + num);
1145 };
1146 
1147 var navigateTo = function(up) {
1148   var numItems = document.querySelectorAll(
1149       ".line > .msgEvent, .line > .msgControl").length;
1150   var currentSelected = findNum();
1151   var newSelected = move(currentSelected, up, numItems);
1152   var newEl = numToId(newSelected, numItems);
1153 
1154   // Scroll element into center.
1155   scrollTo(newEl);
1156 };
1157 
1158 window.addEventListener("keydown", function (event) {
1159   if (event.defaultPrevented) {
1160     return;
1161   }
1162   if (event.key == "j") {
1163     navigateTo(/*up=*/false);
1164   } else if (event.key == "k") {
1165     navigateTo(/*up=*/true);
1166   } else {
1167     return;
1168   }
1169   event.preventDefault();
1170 }, true);
1171 </script>
1172   )<<<";
1173 }
1174