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